monster-rs 0.4.1

Monster is a symbolic execution engine for 64-bit RISC-U code
Documentation
use anyhow::{anyhow, Context, Result};
use clap::{crate_authors, crate_description, crate_version, App, AppSettings, Arg, ArgMatches};
use const_format::formatcp;
use lazy_static::lazy_static;
use monster::{
    engine::{
        rarity_simulation::{defaults as rarity_defaults, MeanType},
        symbolic_execution::defaults as symbolic_defaults,
    },
    path_exploration::ExplorationStrategyType,
    solver::{SmtType, SolverType},
};
use std::str::FromStr;
use strum::{EnumString, EnumVariantNames, IntoStaticStr, VariantNames};

#[derive(Debug, PartialEq, EnumString, EnumVariantNames, IntoStaticStr)]
#[strum(serialize_all = "kebab_case")]
pub enum LogLevel {
    Trace,
    Debug,
    Info,
    Warn,
    Error,
}

lazy_static! {
    static ref COPY_INIT_RATIO: String = format!("{}", rarity_defaults::COPY_INIT_RATIO);
}

pub fn args() -> App<'static, 'static> {
    App::new("Monster")
        .version(crate_version!())
        .author(crate_authors!(", "))
        .about(crate_description!())
        .arg(
            Arg::with_name("verbose")
                .short("v")
                .long("verbose")
                .help("configure logging level to use")
                .takes_value(true)
                .value_name("LEVEL")
                .possible_values(&LogLevel::VARIANTS)
                .default_value(LogLevel::Info.into())
                .global(true),
        )
        .subcommand(
            App::new("disassemble")
                .about("Disassemble a RISC-V ELF binary")
                .arg(
                    Arg::with_name("input-file")
                        .value_name("FILE")
                        .help("Binary file to be disassembled")
                        .takes_value(true)
                        .required(true),
                ),
        )
        .subcommand(
            App::new("cfg")
                .about("Generate control flow graph from RISC-U ELF binary")
                .arg(
                    Arg::with_name("input-file")
                        .help("Source RISC-U binary to be analyzed")
                        .takes_value(true)
                        .value_name("FILE")
                        .required(true),
                )
                .arg(
                    Arg::with_name("output-file")
                        .help("Output file to write to")
                        .short("o")
                        .long("output-file")
                        .takes_value(true)
                        .value_name("FILE")
                        .default_value("cfg.dot"),
                )
                .arg(
                    Arg::with_name("distances")
                        .help("Compute also shortest path distances from exit")
                        .short("d")
                        .long("distances"),
                ),
        )
        .subcommand(
            App::new("execute")
                .about("Symbolically execute a RISC-U ELF binary")
                .arg(
                    Arg::with_name("input-file")
                        .help("RISC-U ELF binary to be executed")
                        .takes_value(true)
                        .value_name("FILE")
                        .required(true),
                )
                .arg(
                    Arg::with_name("solver")
                        .help("SMT solver")
                        .short("s")
                        .long("solver")
                        .takes_value(true)
                        .value_name("SOLVER")
                        .possible_values(&SolverType::VARIANTS)
                        .default_value(SolverType::Monster.into()),
                )
                .arg(
                    Arg::with_name("max-execution-depth")
                        .help("Number of instructions, where the path execution will be aborted")
                        .short("d")
                        .long("execution-depth")
                        .takes_value(true)
                        .value_name("NUMBER")
                        .default_value(formatcp!("{}", symbolic_defaults::MAX_EXECUTION_DEPTH))
                        .validator(is::<u64>),
                )
                .arg(
                    Arg::with_name("memory")
                        .help("Amount of memory to be used per execution context in megabytes [possible_values: 1 .. 1024]")
                        .short("m")
                        .long("memory")
                        .takes_value(true)
                        .value_name("NUMBER")
                        .default_value(formatcp!("{}", symbolic_defaults::MEMORY_SIZE.0 / bytesize::MIB))
                        .validator(is_valid_memory_size),
                )
                .arg(
                    Arg::with_name("strategy")
                    .help("Path exploration strategy to use when exploring state search space")
                    .long("strategy")
                    .takes_value(true)
                    .value_name("STRATEGY")
                    .default_value(ExplorationStrategyType::ShortestPaths.into())
                    .possible_values(ExplorationStrategyType::VARIANTS)
                ),
        )
        .subcommand(
            App::new("smt")
                .about("Create an SMT-lib file for a RISC-U ELF binary")
                .arg(
                    Arg::with_name("input-file")
                        .help("RISC-U ELF binary to be converted")
                        .takes_value(true)
                        .value_name("FILE")
                        .required(true),
                )
                .arg(
                    Arg::with_name("max-execution-depth")
                        .help("Number of instructions, where the path execution will be aborted")
                        .short("d")
                        .long("execution-depth")
                        .takes_value(true)
                        .value_name("NUMBER")
                        .default_value(formatcp!("{}", symbolic_defaults::MAX_EXECUTION_DEPTH))
                        .validator(is::<u64>),
                )
                .arg(
                    Arg::with_name("memory")
                        .help("Amount of memory to be used per execution context in megabytes [possible_values: 1 .. 1024]")
                        .short("m")
                        .long("memory")
                        .takes_value(true)
                        .value_name("NUMBER")
                        .default_value(formatcp!("{}", symbolic_defaults::MEMORY_SIZE.0 / bytesize::MIB))
                        .validator(is_valid_memory_size),
                )
                .arg(
                    Arg::with_name("strategy")
                    .help("Path exploration strategy to use when exploring state search space")
                    .long("strategy")
                    .takes_value(true)
                    .value_name("STRATEGY")
                    .default_value(ExplorationStrategyType::ShortestPaths.into())
                    .possible_values(ExplorationStrategyType::VARIANTS)
                )
                .arg(
                    Arg::with_name("smt-type")
                    .help("Specify a solver type to generate the SMT file for")
                    .long("smt-type")
                    .short("t")
                    .takes_value(true)
                    .default_value(SmtType::Generic.into())
                    .possible_values(SmtType::VARIANTS)
                )
                .arg(
                    Arg::with_name("output-file")
                    .help("Output path for the generated SMT-lib file")
                    .short("o")
                    .long("out")
                    .takes_value(true)
                )
        )
        .subcommand(
            App::new("rarity")
                .about("Performs rarity simulation on a RISC-U ELF binary")
                .arg(
                    Arg::with_name("input-file")
                        .help("Source RISC-U binary to be analyzed")
                        .takes_value(true)
                        .value_name("FILE")
                        .required(true),
                )
                .arg(
                    Arg::with_name("memory")
                        .help("Amount of memory to be used per execution context in megabytes [possible_values: 1 .. 1024]")
                        .short("m")
                        .long("memory")
                        .takes_value(true)
                        .value_name("NUMBER")
                        .default_value(formatcp!("{}", rarity_defaults::MEMORY_SIZE.0 / bytesize::MIB))
                        .validator(is_valid_memory_size),
                )
                .arg(
                    Arg::with_name("step-size")
                        .help("Instructions to be executed for each round")
                        .long("step-size")
                        .takes_value(true)
                        .value_name("NUMBER")
                        .default_value(formatcp!("{}", rarity_defaults::STEP_SIZE))
                        .validator(is::<u64>),
                )
                .arg(
                    Arg::with_name("states")
                        .help("Number of distinct states")
                        .long("states")
                        .takes_value(true)
                        .value_name("NUMBER")
                        .default_value(formatcp!("{}", rarity_defaults::AMOUNT_OF_STATES))
                        .validator(is::<usize>),
                )
                .arg(
                    Arg::with_name("selection")
                    .help("Number of runs to select in every iteration")
                    .short("s")
                    .long("selection")
                    .takes_value(true)
                    .value_name("NUMBER")
                    .default_value(formatcp!("{}", rarity_defaults::SELECTION))
                    .validator(is::<usize>))
                .arg(
                    Arg::with_name("iterations")
                    .help("Iterations of rarity simulation to run")
                    .short("i")
                    .long("iterations")
                    .takes_value(true)
                    .value_name("NUMBER")
                    .default_value(formatcp!("{}", rarity_defaults::ITERATIONS))
                    .validator(is::<u64>))
                .arg(
                    Arg::with_name("copy-init-ratio")
                        .help("Determines how much new states are copied instead of started from the beginning")
                        .long("copy-init-ratio")
                        .takes_value(true)
                        .value_name("RATIO")
                        .default_value(COPY_INIT_RATIO.as_str())
                        .validator(is_ratio)
                    )
                .arg(
                    Arg::with_name("mean")
                    .help("The average to be used for the counts")
                    .long("mean")
                    .takes_value(true)
                    .value_name("MEAN")
                    .possible_values(&MeanType::VARIANTS)
                    .default_value(rarity_defaults::MEAN_TYPE.into())
                    )
        )
        .setting(AppSettings::SubcommandRequiredElseHelp)
        .global_setting(AppSettings::GlobalVersion)
}

pub fn expect_arg<T: FromStr>(m: &ArgMatches, arg: &str) -> Result<T>
where
    <T as FromStr>::Err: Send + Sync + std::error::Error + 'static,
{
    m.value_of(arg)
        .ok_or_else(|| anyhow!("argument \"{}\" has to be set in CLI at all times", arg))
        .and_then(|s| {
            T::from_str(s).with_context(|| format!("argument \"{}\" has wrong format", arg))
        })
}

pub fn expect_optional_arg<T: FromStr>(m: &ArgMatches, arg: &str) -> Result<Option<T>>
where
    <T as FromStr>::Err: Send + Sync + std::error::Error + 'static,
{
    match m.value_of(arg) {
        Some(s) => {
            let res =
                T::from_str(s).with_context(|| format!("argument \"{}\" has wrong format", arg))?;
            Ok(Some(res))
        }
        None => Ok(None),
    }
}

fn is<T: FromStr>(v: String) -> Result<(), String>
where
    <T as FromStr>::Err: std::fmt::Display,
{
    v.parse::<T>().map(|_| ()).map_err(|e| e.to_string())
}

fn is_valid_memory_size(v: String) -> Result<(), String> {
    is::<u64>(v.clone()).and_then(|_| {
        let memory_size = v.parse::<u64>().expect("have checked that already");

        let valid_range = 1_u64..=1024_u64;

        if valid_range.contains(&memory_size) {
            Ok(())
        } else {
            Err(String::from("memory size has to be in range: 1 - 1024"))
        }
    })
}

fn is_ratio(v: String) -> Result<(), String> {
    let valid_range = 0.0_f64..=1.0f64;

    match v.parse::<f64>() {
        Ok(ratio) => {
            if valid_range.contains(&ratio) {
                Ok(())
            } else {
                Err("Expected range between 0.0 and 1.0".to_string())
            }
        }
        Err(err) => Err(err.to_string()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn with_matches<F>(a: Vec<&str>, f: F)
    where
        F: Fn(&ArgMatches),
    {
        let matches = args().get_matches_from(a.clone());

        f(matches.subcommand_matches(a[1]).unwrap())
    }

    #[test]
    fn test_execute_defaults_are_set() {
        with_matches(vec!["monster", "execute", "file.o"], |m| {
            assert!(m.is_present("memory"), "Default memory size is set");
            assert!(
                m.is_present("max-execution-depth"),
                "Default execution depth is set"
            );
            assert!(m.is_present("solver"), "Default solver is set");
        });
    }

    #[test]
    fn test_execute_memory_size_argument() {
        assert!(
            args()
                .get_matches_from_safe(vec!["monster", "execute", "-m", "0", "file.o"])
                .is_err(),
            "Memory size 0 is invalid"
        );

        assert!(
            args()
                .get_matches_from_safe(vec!["monster", "execute", "-m", "-23424", "file.o"])
                .is_err(),
            "Negative memory size is invalid"
        );

        assert!(
            args()
                .get_matches_from_safe(vec!["monster", "execute", "-m", "23424", "file.o"])
                .is_err(),
            "memory size is invalid (out of range)"
        );
    }

    #[test]
    fn test_filename_argument_postitions() {
        assert!(
            args()
                .get_matches_from_safe(vec!["monster", "smt", "-t", "generic", "file.o"])
                .is_ok(),
            "Input file can be declared after flags"
        );

        assert!(
            args()
                .get_matches_from_safe(vec!["monster", "smt", "filename", "-t", "generic"])
                .is_ok(),
            "Input file can be declared before flags"
        );
    }
}