use std::{
collections::HashMap,
convert::Infallible,
fmt,
num::{NonZero, ParseIntError, TryFromIntError},
path::PathBuf,
str::FromStr,
};
use byte_unit::{Byte, Unit};
use serde::Deserialize;
use thiserror::Error;
#[cfg(target_os = "linux")]
pub use crate::isolation::filemap::UhyveIoMode;
#[derive(Debug, Clone)]
pub struct Params {
pub memory_size: GuestMemorySize,
#[cfg(target_os = "linux")]
pub thp: bool,
#[cfg(target_os = "linux")]
pub ksm: bool,
pub cpu_count: CpuCount,
#[cfg(target_os = "linux")]
pub cpu_pm: bool,
#[cfg(target_os = "linux")]
pub pit: bool,
pub gdb_port: Option<u16>,
pub kernel_args: Vec<String>,
pub file_mapping: Vec<String>,
pub tempdir: Option<PathBuf>,
#[cfg(target_os = "linux")]
pub file_isolation: FileSandboxMode,
#[cfg(target_os = "linux")]
pub io_mode: UhyveIoMode,
pub output: Output,
pub stats: bool,
pub env: EnvVars,
pub aslr: bool,
#[cfg(feature = "instrument")]
pub trace_dir: Option<PathBuf>,
pub network: Option<NetworkMode>,
}
impl Default for Params {
fn default() -> Self {
Self {
memory_size: Default::default(),
#[cfg(target_os = "linux")]
thp: false,
#[cfg(target_os = "linux")]
ksm: false,
#[cfg(target_os = "linux")]
pit: false,
cpu_count: Default::default(),
#[cfg(target_os = "linux")]
cpu_pm: false,
gdb_port: Default::default(),
file_mapping: Default::default(),
tempdir: Default::default(),
#[cfg(target_os = "linux")]
file_isolation: FileSandboxMode::default(),
#[cfg(target_os = "linux")]
io_mode: Default::default(),
kernel_args: Default::default(),
output: Default::default(),
stats: false,
env: EnvVars::default(),
aslr: true,
#[cfg(feature = "instrument")]
trace_dir: Default::default(),
network: None,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
pub struct CpuCount(NonZero<u32>);
impl CpuCount {
pub fn get(self) -> u32 {
self.0.get()
}
}
impl Default for CpuCount {
fn default() -> Self {
let default = 1.try_into().unwrap();
Self(default)
}
}
impl fmt::Display for CpuCount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl TryFrom<u32> for CpuCount {
type Error = TryFromIntError;
fn try_from(value: u32) -> Result<Self, Self::Error> {
value.try_into().map(Self)
}
}
impl FromStr for CpuCount {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let count = s.parse()?;
Ok(Self(count))
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
pub struct GuestMemorySize(pub(crate) Byte);
impl GuestMemorySize {
const fn minimum() -> Byte {
Byte::from_u64_with_unit(16, Unit::MiB).unwrap()
}
pub fn get(self) -> usize {
self.0.as_u64().try_into().unwrap()
}
}
impl Default for GuestMemorySize {
fn default() -> Self {
Self(Byte::from_u64_with_unit(64, Unit::MiB).unwrap())
}
}
impl fmt::Display for GuestMemorySize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.get_adjusted_unit(Unit::MiB))
}
}
#[derive(Clone, Debug, Default)]
pub enum Output {
#[default]
StdIo,
File(PathBuf),
Buffer,
None,
}
impl FromStr for Output {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"none" | "None" => Ok(Self::None),
p => Ok(Self::File(p.into())),
}
}
}
#[derive(Error, Debug)]
pub enum InvalidGuestMemorySizeError {
#[error(
"Not enough guest memory. Must be at least {min:#} (is {cur:#.3})",
min = GuestMemorySize::minimum().get_adjusted_unit(Unit::MiB),
cur = .0.get_adjusted_unit(Unit::MiB),
)]
MemoryTooSmall(Byte),
#[error(
"Invalid amount of guest memory. Must be a multiple of 2 MiB (is {cur:#.3})",
cur = .0.get_adjusted_unit(Unit::MiB),
)]
NotAHugepage(Byte),
}
impl TryFrom<Byte> for GuestMemorySize {
type Error = InvalidGuestMemorySizeError;
fn try_from(value: Byte) -> Result<Self, Self::Error> {
if value < Self::minimum() {
Err(InvalidGuestMemorySizeError::MemoryTooSmall(value))
} else if !value
.as_u64()
.is_multiple_of(Byte::from_u64_with_unit(2, Unit::MiB).unwrap().as_u64())
{
Err(InvalidGuestMemorySizeError::NotAHugepage(value))
} else {
Ok(Self(value))
}
}
}
#[derive(Error, Debug)]
pub enum ParseByteError {
#[error(transparent)]
Parse(#[from] byte_unit::ParseError),
#[error(transparent)]
InvalidMemorySize(#[from] InvalidGuestMemorySizeError),
}
impl FromStr for GuestMemorySize {
type Err = ParseByteError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let requested = Byte::from_str(s)?;
let memory_size = requested.try_into()?;
Ok(memory_size)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum EnvVars {
Host,
Set(HashMap<String, String>),
}
impl Default for EnvVars {
fn default() -> Self {
Self::Set(HashMap::new())
}
}
impl<S: AsRef<str> + core::fmt::Debug + PartialEq + PartialEq<&'static str>> TryFrom<&[S]>
for EnvVars
{
type Error = &'static str;
fn try_from(v: &[S]) -> Result<Self, Self::Error> {
if v.iter().any(|i| *i == "host") {
if v.len() != 1 {
warn!(
"Specifying -e host discards all other explicitly specified environment vars"
);
}
return Ok(Self::Host);
}
Ok(Self::Set(v.iter().try_fold(
HashMap::new(),
|mut acc, s| {
if let Some(split) = s.as_ref().split_once("=") {
acc.insert(split.0.to_owned(), split.1.to_owned());
Ok(acc)
} else {
Err("Invalid environment variables parameter format: Must be -e var=value")
}
},
)?))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum NetworkMode {
Tap { name: String },
}
impl TryFrom<String> for NetworkMode {
type Error = &'static str;
fn try_from(netmode: String) -> Result<Self, Self::Error> {
netmode_try_from(netmode)
}
}
impl TryFrom<&str> for NetworkMode {
type Error = &'static str;
fn try_from(netmode: &str) -> Result<Self, Self::Error> {
netmode_try_from(netmode)
}
}
fn netmode_try_from<S: AsRef<str>>(netmode: S) -> Result<NetworkMode, &'static str> {
if netmode.as_ref() == "tap" {
return Ok(NetworkMode::Tap {
name: "tap10".to_string(),
});
}
let (mode, device) = netmode
.as_ref()
.split_once(':')
.ok_or("invalid netmode string. Must be mode:devicename")?;
match mode {
"tap" => Ok(NetworkMode::Tap {
name: device.to_string(),
}),
_ => Err("invalid networking mode"),
}
}
#[cfg(target_os = "linux")]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub enum FileSandboxMode {
None,
#[default]
Normal,
Strict,
}
#[cfg(target_os = "linux")]
impl FromStr for FileSandboxMode {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"none" => Ok(FileSandboxMode::None),
"normal" => Ok(FileSandboxMode::Normal),
"strict" => Ok(FileSandboxMode::Strict),
_ => Err("Unknown file sandbox mode"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_env_vars() {
let strings = [String::from("ASDF=asdf"), String::from("EMOJI=🤷")];
let Ok(EnvVars::Set(map)) = EnvVars::try_from(strings.as_slice()) else {
panic!();
};
assert_eq!(map.get("ASDF").unwrap(), "asdf");
assert_eq!(map.get("EMOJI").unwrap(), "🤷");
let env_vars = EnvVars::try_from(&["host", "OTHER=asdf"] as &[&str]).unwrap();
assert_eq!(env_vars, EnvVars::Host);
}
#[test]
#[cfg(target_os = "linux")]
fn test_file_sandbox_mode() {
let mut mode = FileSandboxMode::from_str("none");
assert_eq!(mode, Ok(FileSandboxMode::None));
mode = FileSandboxMode::from_str("normal");
assert_eq!(mode, Ok(FileSandboxMode::Normal));
mode = FileSandboxMode::from_str("strict");
assert_eq!(mode, Ok(FileSandboxMode::Strict));
}
}