use crate::abi::Arch;
use crate::fs::{DevFs, MountTable, ProcFs, SysFs, TmpFs, tar};
use crate::kernel::{Kernel, Pumped};
use crate::loader::{ProcessSpec, interp_path, load_dynamic, load_static};
use crate::vcpu::GuestMemory;
use crate::vcpu::mem::PAGE_SIZE;
use std::sync::{Arc, Mutex};
const GUEST_BASE: u64 = 0x1_0000;
struct CaptureSink(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for CaptureSink {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[derive(Debug, Default)]
pub struct Step {
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub exit_code: Option<i32>,
}
#[derive(Debug)]
pub struct Vm {
kernel: Kernel,
stdout: Arc<Mutex<Vec<u8>>>,
stderr: Arc<Mutex<Vec<u8>>>,
finished: Option<i32>,
}
impl Vm {
pub fn boot(tar_bytes: &[u8], argv: Vec<String>, mem_bytes: u64) -> Result<Self, String> {
let mut mounts = MountTable::new();
mounts.mount("/", Box::new(TmpFs::new()));
tar::extract_into(&mut mounts, tar_bytes);
Self::finish(mounts, argv, mem_bytes)
}
#[cfg(feature = "fstool")]
pub fn boot_squashfs(
tar_bytes: &[u8],
argv: Vec<String>,
mem_bytes: u64,
) -> Result<Self, String> {
use crate::fs::{FsToolMount, Overlay};
let lower = FsToolMount::from_tar(tar_bytes)
.map_err(|e| format!("build squashfs from rootfs tar: {e}"))?;
let mut mounts = MountTable::new();
mounts.mount(
"/",
Box::new(Overlay::new(Box::new(lower), Box::new(TmpFs::new()))),
);
Self::finish(mounts, argv, mem_bytes)
}
fn finish(mut mounts: MountTable, argv: Vec<String>, mem_bytes: u64) -> Result<Self, String> {
if argv.is_empty() {
return Err("empty argv".into());
}
let path = argv[0].clone();
mounts.mount("/tmp", Box::new(TmpFs::new()));
mounts.mount("/dev", Box::new(DevFs::new()));
mounts.mount("/proc", Box::new(ProcFs::new()));
mounts.mount("/sys", Box::new(SysFs::new()));
let elf = read_mount_file(&mut mounts, &path)
.ok_or_else(|| format!("{path} not found in the root image"))?;
let arch = detect_arch(&elf).ok_or("unrecognized ELF machine type")?;
let mut mem = GuestMemory::new(GUEST_BASE, round_up_page(mem_bytes));
let spec = ProcessSpec {
argv,
envp: default_env(),
};
let loaded = if let Some(interp) = interp_path(&elf) {
let interp_elf = read_mount_file(&mut mounts, &interp)
.ok_or_else(|| format!("dynamic linker {interp} not found in the root image"))?;
load_dynamic(&mut mem, &elf, &interp_elf, &spec)
} else {
load_static(&mut mem, &elf, &spec)
};
let img = loaded.map_err(|e| e.to_string())?;
let mid = page_down(img.program_break + (img.stack_bottom - img.program_break) / 2);
let backend = select_backend(arch)?;
let vcpu = backend
.new_vcpu(img.entry, img.stack_pointer)
.map_err(|e| e.to_string())?;
let stdout = Arc::new(Mutex::new(Vec::new()));
let stderr = Arc::new(Mutex::new(Vec::new()));
let mut kernel = Kernel::new(arch, mounts);
kernel.set_interactive(true);
kernel.set_stdout(Box::new(CaptureSink(stdout.clone())));
kernel.set_stderr(Box::new(CaptureSink(stderr.clone())));
kernel.set_cwd("/root");
kernel.set_heap(img.program_break, mid);
kernel.set_mmap_area(img.stack_bottom, mid);
kernel.boot(vcpu, mem);
Ok(Self {
kernel,
stdout,
stderr,
finished: None,
})
}
pub fn write_stdin(&mut self, bytes: &[u8]) {
self.kernel.feed_stdin(bytes);
}
pub fn close_stdin(&mut self) {
self.kernel.close_stdin();
}
#[must_use]
pub fn exit_code(&self) -> Option<i32> {
self.finished
}
pub fn pump(&mut self) -> Result<Step, String> {
if self.finished.is_none() {
match self.kernel.pump().map_err(|e| e.to_string())? {
Pumped::Exited(code) => self.finished = Some(code),
Pumped::Blocked => {}
}
}
Ok(Step {
stdout: std::mem::take(&mut *self.stdout.lock().unwrap()),
stderr: std::mem::take(&mut *self.stderr.lock().unwrap()),
exit_code: self.finished,
})
}
}
fn read_mount_file(mounts: &mut MountTable, path: &str) -> Option<Vec<u8>> {
let size = mounts.stat(path)?.size as usize;
let mut buf = vec![0u8; size];
let mut off = 0;
while off < size {
match mounts.read_at(path, off as u64, &mut buf[off..]) {
Ok(0) => break,
Ok(n) => off += n,
Err(_) => return None,
}
}
buf.truncate(off);
Some(buf)
}
fn detect_arch(elf: &[u8]) -> Option<Arch> {
const EM_X86_64: u16 = 62;
const EM_AARCH64: u16 = 183;
match u16::from_le_bytes([*elf.get(18)?, *elf.get(19)?]) {
EM_AARCH64 => Some(Arch::Aarch64),
EM_X86_64 => Some(Arch::X86_64),
_ => None,
}
}
fn select_backend(arch: Arch) -> Result<Box<dyn crate::vcpu::Backend>, String> {
use crate::vcpu::Backend;
match arch {
Arch::Aarch64 => crate::vcpu::interp::InterpBackend::new(arch)
.map(|b| Box::new(b) as Box<dyn Backend>)
.map_err(|e| e.to_string()),
Arch::X86_64 => crate::vcpu::interp_x86::X86Backend::new(arch)
.map(|b| Box::new(b) as Box<dyn Backend>)
.map_err(|e| e.to_string()),
}
}
fn default_env() -> Vec<String> {
vec![
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string(),
"HOME=/root".to_string(),
"TERM=xterm-256color".to_string(),
"PS1=nixvm:\\w\\$ ".to_string(),
"PWD=/root".to_string(),
"USER=root".to_string(),
"HOSTNAME=nixvm".to_string(),
]
}
fn round_up_page(v: u64) -> u64 {
v.div_ceil(PAGE_SIZE) * PAGE_SIZE
}
fn page_down(v: u64) -> u64 {
v - v % PAGE_SIZE
}