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
435 changes: 253 additions & 182 deletions Cargo.lock

Large diffs are not rendered by default.

32 changes: 21 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
## Using

``` sh
Usage: muvm [-c=CPU_LIST]... [-e=ENV]... [--mem=MEM] [--vram=VRAM] [--passt-socket=PATH] [-p=
SERVER_PORT] [-f=FEX_IMAGE]... COMMAND [COMMAND_ARGS]...
Usage: muvm [-c=CPU_LIST]... [-e=ENV]... [--mem=MEM] [--vram=VRAM] [--passt-socket=PATH] [-f=
FEX_IMAGE]... [-m] [--no-tty] [--privileged] [-p=<[[IP:][HOST_PORT]:]GUEST_PORT[/PROTOCOL]>]... [
--emu=EMU] [--no-inherit-env] COMMAND [COMMAND_ARGS]...

Available positional items:
COMMAND the command you want to execute in the vm
Expand All @@ -34,19 +35,28 @@ Available options:
Machine Monitor) will attempt to return as many pages as
possible to the host.
[default: 80% of total RAM]
--vram=VRAM The amount of Video RAM, in MiB, that will be available to this
microVM.
The memory configured for the microVM will not be reserved
immediately. Instead, it will be provided as the guest demands
it, and will be returned to the host once the guest releases
the underlying resources.
[default: same as the total amount of RAM in the system]
--vram=VRAM The amount of Video RAM, in MiB, that will reported by userspace in
this microVM.
The userspace drivers will report this amount as heap size
to the clients running in the microVM.
[default: 50% of total RAM]
--passt-socket=PATH Instead of starting passt, connect to passt socket at PATH
-p, --server-port=SERVER_PORT Set the port to be used in server mode
[default: 3334]
-f, --fex-image=FEX_IMAGE Adds an erofs file to be mounted as a FEX rootfs.
May be specified multiple times.
First the base image, then overlays in order.
-m, --merged-rootfs Use merged rootfs for FEX (experimental)
--no-tty Force not allocating a tty for the command
--privileged Run the command as root inside the vm.
This notably does not allow root access to the host fs.
-p, --publish=<[[IP:][HOST_PORT]:]GUEST_PORT[/PROTOCOL]>
Publish a guest’s port, or range of ports, to the host.
The syntax is similar to podman/docker.
--emu=EMU Which emulator to use for running x86_64 binaries.
Valid options are "box" and "fex". If this argument is not
present, muvm will try to use FEX, falling back to Box if it
can't be found.
--no-inherit-env Run the command with as little of the environment variables
passed in as possible, instead of inheriting most of them.
-h, --help Prints help information
```

Expand Down
2 changes: 1 addition & 1 deletion crates/muvm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ rustix = { version = "0.38.34", default-features = false, features = ["fs", "mou
serde = { version = "1.0.203", default-features = false, features = ["derive"] }
serde_json = { version = "1.0.117", default-features = false, features = ["std"] }
tempfile = { version = "3.10.1", default-features = false, features = [] }
tokio = { version = "1.38.0", default-features = false, features = ["io-util", "macros", "net", "process", "rt-multi-thread", "sync"] }
tokio = { version = "1.38.0", default-features = false, features = ["io-util", "macros", "net", "process", "rt-multi-thread", "sync", "time"] }
tokio-stream = { version = "0.1.15", default-features = false, features = ["net", "sync"] }
udev = { version = "0.9.0", default-features = false, features = [] }
uuid = { version = "1.10.0", default-features = false, features = ["serde", "std", "v7"] }
Expand Down
102 changes: 77 additions & 25 deletions crates/muvm/src/bin/muvm.rs
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
use std::collections::HashMap;
use std::convert::Infallible;
use std::env;
use std::ffi::{c_char, CString};
use std::fs::{self, File};
use std::io::Write;
use std::os::fd::{IntoRawFd, OwnedFd};
use std::os::fd::{FromRawFd, IntoRawFd, OwnedFd};
use std::path::Path;
use std::process::ExitCode;
use std::{env, fs};
use std::process::{Command, ExitCode};

use anyhow::{anyhow, Context, Result};
use krun_sys::{
krun_add_disk, krun_add_virtiofs2, krun_add_vsock_port, krun_add_vsock_port2, krun_create_ctx,
krun_set_env, krun_set_gpu_options2, krun_set_log_level, krun_set_passt_fd, krun_set_root,
krun_set_vm_config, krun_set_workdir, krun_start_enter, VIRGLRENDERER_DRM,
VIRGLRENDERER_THREAD_SYNC, VIRGLRENDERER_USE_ASYNC_FENCE_CB, VIRGLRENDERER_USE_EGL,
krun_set_console_output, krun_set_env, krun_set_gpu_options2, krun_set_log_level,
krun_set_passt_fd, krun_set_root, krun_set_vm_config, krun_set_workdir, krun_start_enter,
VIRGLRENDERER_DRM, VIRGLRENDERER_THREAD_SYNC, VIRGLRENDERER_USE_ASYNC_FENCE_CB,
VIRGLRENDERER_USE_EGL,
};
use log::debug;
use muvm::cli_options::options;
use muvm::cli_options::{options, Options};
use muvm::cpu::{get_fallback_cores, get_performance_cores};
use muvm::env::{find_muvm_exec, prepare_env_vars};
use muvm::hidpipe_server::spawn_hidpipe_server;
use muvm::launch::{launch_or_lock, LaunchResult, DYNAMIC_PORT_RANGE};
use muvm::monitor::spawn_monitor;
use muvm::net::{connect_to_passt, start_passt};
use muvm::types::MiB;
use muvm::utils::launch::{
GuestConfiguration, Launch, HIDPIPE_SOCKET, MUVM_GUEST_SOCKET, PULSE_SOCKET,
};
use muvm::utils::launch::{GuestConfiguration, HIDPIPE_SOCKET, MUVM_GUEST_SOCKET, PULSE_SOCKET};
use nix::fcntl::{fcntl, FcntlArg};
use nix::sys::sysinfo::sysinfo;
use nix::unistd::User;
use rustix::io::Errno;
use rustix::io::{dup, Errno};
use rustix::process::{
geteuid, getgid, getrlimit, getuid, sched_setaffinity, setrlimit, CpuSet, Resource,
};
use serde::Serialize;
use tempfile::NamedTempFile;
use uuid::Uuid;

const LOCK_FD_ENV_VAR: &str = "__MUVM_DO_LAUNCH_VM_LOCK__";

#[derive(Serialize)]
pub struct KrunConfig {
Expand Down Expand Up @@ -73,14 +77,28 @@ fn main() -> Result<ExitCode> {
}

let options = options().fallback_to_usage().run();
if let Ok(lock_fd) = env::var(LOCK_FD_ENV_VAR) {
let lock_fd = lock_fd.parse()?;
fcntl(lock_fd, FcntlArg::F_GETFD).context("Lockfile fd is not open")?;
// SAFETY: We verify that the file descriptor is valid.
// The file will not be read from/written to,
// so worse case if it's not a file nothing bad will happen,
// as we only care about calling `close` on Drop.
let _lock_file = unsafe { File::from_raw_fd(lock_fd) };
launch_vm(options)?;
unreachable!("`launch_vm` never returns");
}

let (_lock_file, command, command_args, env) = match launch_or_lock(
let cwd = env::current_dir()?;
let inherit_env = !options.no_inherit_env;
let (lock_file, command, command_args, env, cwd) = match launch_or_lock(
options.command,
options.command_args,
options.env,
options.interactive,
options.tty,
cwd,
options.no_tty,
options.privileged,
inherit_env,
)? {
LaunchResult::LaunchRequested(code) => {
// There was a muvm instance already running and we've requested it
Expand All @@ -92,11 +110,33 @@ fn main() -> Result<ExitCode> {
command,
command_args,
env,
} => (lock_file, command, command_args, env),
cwd,
} => (lock_file, command, command_args, env, cwd),
};

let mut env = prepare_env_vars(env).context("Failed to prepare environment variables")?;
// Make it lose CLOEXEC
let lock_fd = dup(lock_file)?;
Command::new(env::current_exe()?)
.args(env::args())
.env(LOCK_FD_ENV_VAR, lock_fd.into_raw_fd().to_string())
.spawn()?;
match launch_or_lock(
command,
command_args,
env,
cwd,
options.no_tty,
options.privileged,
inherit_env,
)? {
LaunchResult::LockAcquired { .. } => Err(anyhow!("VM did not start")),
LaunchResult::LaunchRequested(code) => Ok(code),
}
}

fn launch_vm(options: Options) -> Result<Infallible> {
let mut env =
prepare_env_vars(Vec::new(), false).context("Failed to prepare environment variables")?;
{
// Set the log level to "off".
//
Expand Down Expand Up @@ -277,6 +317,7 @@ fn main() -> Result<ExitCode> {
}
}

let console_base;
if let Ok(run_path) = env::var("XDG_RUNTIME_DIR") {
let pulse_path = Path::new(&run_path).join("pulse/native");
if pulse_path.exists() {
Expand Down Expand Up @@ -344,6 +385,9 @@ fn main() -> Result<ExitCode> {
let err = Errno::from_raw_os_error(-err);
return Err(err).context("Failed to configure vsock for guest server socket");
}
console_base = run_path;
} else {
console_base = "/tmp".to_string();
}

let username = env::var("USER").context("Failed to get username from environment")?;
Expand Down Expand Up @@ -381,14 +425,6 @@ fn main() -> Result<ExitCode> {

let display = env::var("DISPLAY").ok();
let guest_config = GuestConfiguration {
command: Launch {
command,
command_args,
env: HashMap::new(),
vsock_port: 0,
tty: false,
privileged: false,
},
username,
uid: getuid().as_raw(),
gid: getgid().as_raw(),
Expand Down Expand Up @@ -463,6 +499,22 @@ fn main() -> Result<ExitCode> {
}
}

{
let uuid = Uuid::now_v7();
let path = Path::new(&console_base).join(format!("muvm-{uuid}.console"));
let console_path = CString::new(
path.to_str()
.expect("console_path should not contain invalid UTF-8"),
)
.expect("console_path should not contain NUL character");
// SAFETY: `console_path` is a CString that outlives this call
let err = unsafe { krun_set_console_output(ctx_id, console_path.as_ptr()) };
if err < 0 {
let err = Errno::from_raw_os_error(-err);
return Err(err).context("Failed to configure console");
}
}

spawn_monitor();

{
Expand Down
23 changes: 12 additions & 11 deletions crates/muvm/src/cli_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ pub struct Options {
pub passt_socket: Option<PathBuf>,
pub fex_images: Vec<String>,
pub merged_rootfs: bool,
pub interactive: bool,
pub tty: bool,
pub no_tty: bool,
pub privileged: bool,
pub publish_ports: Vec<String>,
pub emulator: Option<Emulator>,
pub no_inherit_env: bool,
pub command: PathBuf,
pub command_args: Vec<String>,
}
Expand Down Expand Up @@ -109,13 +109,8 @@ pub fn options() -> OptionParser<Options> {
.help("Instead of starting passt, connect to passt socket at PATH")
.argument("PATH")
.optional();
let interactive = long("interactive")
.short('i')
.help("Attach to the command's stdin/out after starting it")
.switch();
let tty = long("tty")
.short('t')
.help("Allocate a tty for the command")
let no_tty = long("no-tty")
.help("Force not allocating a tty for the command")
.switch();
let privileged = long("privileged")
.help(
Expand All @@ -132,6 +127,12 @@ pub fn options() -> OptionParser<Options> {
)
.argument::<String>("[[IP:][HOST_PORT]:]GUEST_PORT[/PROTOCOL]")
.many();
let no_inherit_env = long("no-inherit-env")
.help(
"Run the command with as little of the environment variables
passed in as possible, instead of inheriting most of them.",
)
.switch();
let command = positional("COMMAND").help("the command you want to execute in the vm");
let command_args = any::<String, _, _>("COMMAND_ARGS", |arg| {
(!["--help", "-h"].contains(&&*arg)).then_some(arg)
Expand All @@ -147,11 +148,11 @@ pub fn options() -> OptionParser<Options> {
passt_socket,
fex_images,
merged_rootfs,
interactive,
tty,
no_tty,
privileged,
publish_ports,
emulator,
no_inherit_env,
// positionals
command,
command_args,
Expand Down
Loading