// SPDX-License-Identifier: EUPL-1.2+ // SPDX-FileCopyrightText: 2022 Alyssa Ross mod ch; pub mod fs; mod net; use std::borrow::Cow; use std::env::args_os; use std::ffi::{CString, OsStr, OsString}; use std::fs::read_dir; use std::io::{self, ErrorKind}; use std::os::unix::prelude::*; use std::path::{Path, PathBuf}; use std::process::Command; use fs::{OwnedFdExt, Root}; use net::{format_mac, net_setup, NetConfig}; pub fn prog_name() -> String { args_os() .next() .as_ref() .map(Path::new) .and_then(Path::file_name) .map(OsStr::to_string_lossy) .unwrap_or(Cow::Borrowed("start-vm")) .into_owned() } pub fn vm_command(dir: PathBuf, root: &Root) -> Result { let dir = dir.into_os_string().into_vec(); let dir = PathBuf::from(OsString::from_vec(dir)); let vm_name = dir .file_name() .ok_or_else(|| "directory has no name".to_string())?; if vm_name.as_bytes().contains(&b',') { return Err(format!("VM name may not contain a comma: {:?}", vm_name)); } let config = root .resolve_no_cloexec(Path::new("svc/data").join(vm_name)) .and_then(|fd| fd.path()) .map_err(|e| format!("resolving configuration directory for {:?}: {}", vm_name, e))?; let mut command = Command::new("s6-notifyoncheck"); command.args(&["-dc", "test -S env/cloud-hypervisor.sock"]); command.arg("cloud-hypervisor"); command.args(&["--api-socket", "env/cloud-hypervisor.sock"]); command.args(&["--cmdline", "console=ttyS0 root=PARTLABEL=root"]); command.args(&["--memory", "size=128M,shared=on"]); command.args(&["--console", "pty"]); command.arg("--kernel"); command.arg(config.join("vmlinux")); match read_dir(config.join("providers/net")) { Ok(entries) => { for r in entries { let entry = r .map_err(|e| format!("examining directory entry: {}", e))? .file_name(); // Safe because provider_name is the name of a directory entry, so // can't contain a null byte. let provider_name = unsafe { CString::from_vec_unchecked(entry.into_vec()) }; // Safe because we pass a valid pointer and check the result. let NetConfig { fd, mac } = unsafe { net_setup(provider_name.as_ptr()) }; if fd == -1 { let e = io::Error::last_os_error(); return Err(format!("setting up networking failed: {}", e)); } command .arg("--net") .arg(format!("fd={},mac={}", fd, format_mac(&mac))); // TODO: to support multiple net providers, we'll need // a better naming scheme for tap and bridge devices. break; } } Err(e) if e.kind() == ErrorKind::NotFound => {} Err(e) => return Err(format!("reading providers/net: {}", e)), } command.arg("--disk"); match read_dir(config.join("blk")) { Ok(entries) => { for result in entries { let entry = result .map_err(|e| format!("examining directory entry: {}", e))? .path(); if entry.extension() != Some(OsStr::new("img")) { continue; } if entry.as_os_str().as_bytes().contains(&b',') { return Err(format!("illegal ',' character in path {:?}", entry)); } let mut arg = OsString::from("path="); arg.push(entry); arg.push(",readonly=on"); command.arg(arg); } } Err(e) => return Err(format!("reading blk: {}", e)), } if command.get_args().last() == Some(OsStr::new("--disk")) { return Err("no block devices specified".to_string()); } match read_dir(config.join("shared-dirs")).map(Iterator::peekable) { Ok(mut entries) => { if entries.peek().is_some() { command.arg("--fs"); } for result in entries { let entry = result .map_err(|e| format!("examining directory entry: {}", e))? .file_name(); let mut arg = OsString::from("tag="); arg.push(&entry); arg.push(",socket=../"); arg.push(vm_name); arg.push("-fs-"); arg.push(&entry); arg.push("/env/virtiofsd.sock"); command.arg(arg); } } Err(e) if e.kind() == ErrorKind::NotFound => {} Err(e) => return Err(format!("reading shared-dirs: {}", e)), } command.arg("--serial").arg({ let mut serial = OsString::from("file=/run/"); serial.push(&vm_name); serial.push(".log"); serial }); Ok(command) } #[cfg(test)] mod tests { use super::*; #[test] fn test_vm_name_comma() { assert!(vm_command("/v,m".into(), &Root::open("/").unwrap()) .unwrap_err() .contains("comma")); } }