symcc_runtime 0.8.2

Build Concolic Tracing tools based on SymCC in Rust
Documentation
use std::{
    env,
    fs::File,
    io::Write,
    path::{Path, PathBuf},
    process::exit,
};

use lazy_static::lazy_static;
use regex::{Regex, RegexBuilder};
use symcc_libafl::clone_symcc;

const SYMCC_RUNTIME_FUNCTION_NAME_PREFIX: &str = "_cpp_";

lazy_static! {
    static ref FUNCTION_NAME_REGEX: Regex = Regex::new(r"pub fn (\w+)").unwrap();
    static ref EXPORTED_FUNCTION_REGEX: Regex = RegexBuilder::new(r"(pub fn \w+\([^\)]*\)[^;]*);")
        .multi_line(true)
        .build()
        .unwrap();
}

fn main() {
    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    let symcc_src_path = checkout_symcc(&out_path);

    write_rust_runtime_macro_file(&out_path, &symcc_src_path);

    if env::var("TARGET").unwrap().contains("linux") {
        let cpp_bindings = bindgen::Builder::default()
            .clang_arg(format!(
                "-I{}",
                symcc_src_path.join("runtime").to_str().unwrap()
            ))
            .clang_arg(format!(
                "-I{}",
                symcc_src_path
                    .join("runtime")
                    .join("rust_backend")
                    .to_str()
                    .unwrap()
            ))
            .clang_args(["-x", "c++", "-std=c++17"].iter())
            .header(
                symcc_src_path
                    .join("runtime")
                    .join("rust_backend")
                    .join("Runtime.h")
                    .to_str()
                    .unwrap(),
            )
            .header(
                symcc_src_path
                    .join("runtime")
                    .join("LibcWrappers.cpp")
                    .to_str()
                    .unwrap(),
            )
            .allowlist_type("SymExpr")
            .allowlist_function("(_sym_.*)|(.*_symbolized)")
            .opaque_type("_.*")
            .size_t_is_usize(true)
            .generate()
            .expect("Unable to generate bindings");

        write_symcc_runtime_bindings_file(&out_path, &cpp_bindings);
        write_cpp_function_export_macro(&out_path, &cpp_bindings);

        if std::env::var("CARGO_FEATURE_NO_CPP_RUNTIME").is_err()
            && std::env::var("DOCS_RS").is_err()
        {
            let rename_header_path = out_path.join("rename.h");
            write_symcc_rename_header(&rename_header_path, &cpp_bindings);
            build_and_link_symcc_runtime(&symcc_src_path, &rename_header_path);
        }
    } else {
        println!("cargo:warning=Building SymCC is only supported on Linux");
    }
}

fn write_cpp_function_export_macro(out_path: &Path, cpp_bindings: &bindgen::Bindings) {
    let mut macro_file = File::create(out_path.join("cpp_exports_macro.rs")).unwrap();
    writeln!(
        macro_file,
        "#[doc(hidden)]
        #[macro_export]
        macro_rules! export_cpp_runtime_functions {{
            () => {{",
    )
    .unwrap();
    EXPORTED_FUNCTION_REGEX
        .captures_iter(&cpp_bindings.to_string())
        .for_each(|captures| {
            writeln!(
                macro_file,
                "    symcc_runtime::export_c_symbol!({});",
                &captures[1]
            )
            .unwrap();
        });
    writeln!(
        macro_file,
        " }};
        }}",
    )
    .unwrap();
}

fn checkout_symcc(out_path: &Path) -> PathBuf {
    if std::env::var("DOCS_RS").is_ok() {
        "symcc".into()
    } else {
        let repo_dir = out_path.join("libafl_symcc_src");
        if !repo_dir.exists() {
            clone_symcc(&repo_dir);
        }
        repo_dir
    }
}

fn write_rust_runtime_macro_file(out_path: &Path, symcc_src_path: &Path) {
    let rust_bindings = bindgen::Builder::default()
        .clang_arg(format!(
            "-I{}",
            symcc_src_path.join("runtime").to_str().unwrap()
        ))
        .clang_arg(format!(
            "-I{}",
            symcc_src_path
                .join("runtime")
                .join("rust_backend")
                .to_str()
                .unwrap()
        ))
        .clang_args(["-x", "c++", "-std=c++17"].iter())
        .header(
            symcc_src_path
                .join("runtime")
                .join("rust_backend")
                .join("RustRuntime.h")
                .to_str()
                .unwrap(),
        )
        .allowlist_type("RSymExpr")
        .allowlist_function("_rsym_.*")
        .opaque_type("_.*")
        .size_t_is_usize(true)
        .generate()
        .expect("Unable to generate bindings");
    let mut rust_runtime_macro = File::create(out_path.join("rust_exports_macro.rs")).unwrap();
    writeln!(
        rust_runtime_macro,
        "#[doc(hidden)]
        #[macro_export]
        macro_rules! invoke_macro_with_rust_runtime_exports {{
            ($macro:path; $($extra_ident:path),*) => {{",
    )
    .unwrap();
    EXPORTED_FUNCTION_REGEX
        .captures_iter(&rust_bindings.to_string())
        .for_each(|captures| {
            writeln!(
                rust_runtime_macro,
                "    $macro!({},{}; $($extra_ident),*);",
                &captures[1].replace("_rsym_", ""),
                &FUNCTION_NAME_REGEX.captures(&captures[1]).unwrap()[1]
            )
            .unwrap();
        });
    writeln!(
        rust_runtime_macro,
        " }};
        }}",
    )
    .unwrap();
}

fn write_symcc_runtime_bindings_file(out_path: &Path, cpp_bindings: &bindgen::Bindings) {
    let mut bindings_file = File::create(out_path.join("bindings.rs")).unwrap();
    cpp_bindings.to_string().lines().for_each(|l| {
        if let Some(captures) = FUNCTION_NAME_REGEX.captures(l) {
            let function_name = &captures[1];
            writeln!(
                bindings_file,
                "#[link_name=\"{}{}\"]",
                SYMCC_RUNTIME_FUNCTION_NAME_PREFIX, function_name
            )
            .unwrap();
        }
        writeln!(bindings_file, "{l}").unwrap();
    });
}

fn write_symcc_rename_header(rename_header_path: &Path, cpp_bindings: &bindgen::Bindings) {
    let mut rename_header_file = File::create(rename_header_path).unwrap();
    writeln!(
        rename_header_file,
        "#ifndef PREFIX_EXPORTS_H
        #define PREFIX_EXPORTS_H",
    )
    .unwrap();

    cpp_bindings
        .to_string()
        .lines()
        .filter_map(|l| FUNCTION_NAME_REGEX.captures(l))
        .map(|captures| captures[1].to_string())
        .for_each(|val| {
            writeln!(
                rename_header_file,
                "#define {} {SYMCC_RUNTIME_FUNCTION_NAME_PREFIX}{}",
                &val, &val
            )
            .unwrap();
        });

    writeln!(rename_header_file, "#endif").unwrap();
}

fn build_and_link_symcc_runtime(symcc_src_path: &Path, rename_header_path: &Path) {
    build_dep_check(&["cmake"]);
    let cpp_lib = cmake::Config::new(symcc_src_path.join("runtime"))
        .define("RUST_BACKEND", "ON")
        .cxxflag(format!(
            "-include \"{}\"",
            rename_header_path.to_str().unwrap()
        ))
        .build()
        .join("lib");
    link_with_cpp_stdlib();
    println!("cargo:rustc-link-search=native={}", cpp_lib.display());
    println!("cargo:rustc-link-lib=static=SymRuntime");
}

fn link_with_cpp_stdlib() {
    let target = env::var("TARGET").unwrap();
    if target.contains("apple") {
        println!("cargo:rustc-link-lib=dylib=c++");
    } else if target.contains("linux") {
        println!("cargo:rustc-link-lib=dylib=stdc++");
    } else {
        unimplemented!();
    }
}

fn build_dep_check(tools: &[&str]) {
    for tool in tools {
        println!("Checking for build tool {tool}...");

        if let Ok(path) = which::which(tool) {
            println!("Found build tool {}", path.to_str().unwrap());
        } else {
            println!("ERROR: missing build tool {tool}");
            exit(1);
        };
    }
}