libafl_libfuzzer 0.16.1

libFuzzer shim which uses LibAFL with common defaults
Documentation
#[cfg(any(target_os = "linux", target_os = "macos"))]
mod unix {
    use core::error::Error;
    use std::{
        fs,
        fs::File,
        io::{BufRead, BufReader, BufWriter, Write},
        os::unix::fs::symlink,
        path::{Path, PathBuf},
        process::{Command, Stdio},
    };

    #[cfg(feature = "rabbit")]
    const NAMESPACE: &str = "🐇";
    #[cfg(not(feature = "rabbit"))]
    const NAMESPACE: &str = "__libafl";
    const NAMESPACE_LEN: usize = NAMESPACE.len();
    const RUNTIME_CRATE_NAME: &str = "libafl_libfuzzer_runtime";

    #[expect(clippy::too_many_lines)]
    pub fn build() -> Result<(), Box<dyn Error>> {
        println!("cargo:rerun-if-changed=libafl_libfuzzer_runtime/src");
        println!("cargo:rerun-if-changed=libafl_libfuzzer_runtime/build.rs");

        let custom_lib_dir =
            AsRef::<Path>::as_ref(&std::env::var_os("OUT_DIR").unwrap()).join("libafl_libfuzzer");
        let custom_lib_target = custom_lib_dir.join("target");
        fs::create_dir_all(&custom_lib_target)
            .expect("Couldn't create the output directory for the fuzzer runtime build");

        let manifest_dir = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap());

        let mut command = Command::new(std::env::var_os("CARGO").unwrap());
        command
            .env_remove("RUSTFLAGS")
            .env_remove("CARGO_ENCODED_RUSTFLAGS");

        for (var, _) in std::env::vars() {
            if var.starts_with("CARGO_PKG_") || var.starts_with("CARGO_FEATURE_") {
                command.env_remove(var);
            }
        }

        command.env("PATH", std::env::var_os("PATH").unwrap());

        command.arg("build");

        let mut features = vec![];

        if cfg!(any(feature = "fork")) {
            features.push("fork");
        }

        if !features.is_empty() {
            command.arg("--features").arg(features.join(","));
        }

        command
            .arg("--release")
            .arg("--no-default-features")
            .arg("--target-dir")
            .arg(&custom_lib_target)
            .arg("--target")
            .arg(std::env::var_os("TARGET").unwrap());

        // detect if we are a version or path/git dep, or testing version-based behavior
        let runtime_dir = manifest_dir.ancestors().find_map(|ancestor| {
            let direct = ancestor.join(RUNTIME_CRATE_NAME);
            if fs::exists(&direct).unwrap_or(false) {
                return Some(direct);
            }
            let under_crates = ancestor.join("crates").join(RUNTIME_CRATE_NAME);
            if fs::exists(&under_crates).unwrap_or(false) {
                return Some(under_crates);
            }
            None
        });

        if let Some(runtime_dir) =
            runtime_dir.filter(|_| !cfg!(feature = "libafl-libfuzzer-use-version"))
        {
            command.current_dir(runtime_dir);
        } else {
            // we are being used as a version dep; we need to create the package virtually

            // remove old files; we need to trigger a rebuild if our path changes!
            let _ = fs::remove_file(custom_lib_dir.join("src"));
            let _ = fs::remove_dir_all(custom_lib_dir.join("src")); // maybe a dir in windows
            let _ = fs::remove_file(custom_lib_dir.join("build.rs"));
            let _ = fs::remove_file(custom_lib_dir.join("Cargo.toml"));

            // create symlinks for all the source files
            // canonicalize can theoretically fail if we are within a non-executable directory?
            symlink(fs::canonicalize("runtime/src")?, custom_lib_dir.join("src"))?;
            symlink(
                fs::canonicalize("runtime/build.rs")?,
                custom_lib_dir.join("build.rs"),
            )?;

            let mut template: toml::Value =
                toml::from_str(&fs::read_to_string("runtime/Cargo.toml.template")?)?;
            let toml::Value::Table(root) = &mut template else {
                unreachable!("Invalid Cargo.toml");
            };
            root.insert(
                "workspace".to_string(),
                toml::Value::Table(toml::Table::new()),
            );
            let Some(toml::Value::Table(deps)) = root.get_mut("dependencies") else {
                unreachable!("Invalid Cargo.toml");
            };
            let version = env!("CARGO_PKG_VERSION");
            for (_name, spec) in deps {
                if let toml::Value::Table(spec) = spec {
                    // replace all path deps with version deps
                    if spec.remove("path").is_some() {
                        spec.insert(
                            "version".to_string(),
                            toml::Value::String(version.to_string()),
                        );
                    }
                }
            }

            let serialized = toml::to_string(&template)?;
            fs::write(custom_lib_dir.join("Cargo.toml"), serialized)?;

            // build in this filled out template
            command.current_dir(custom_lib_dir);
        }

        assert!(
            command.status().is_ok_and(|s| s.success()),
            "Couldn't build runtime crate! Did you remember to use nightly? (`rustup default nightly` to install)"
        );

        let redefined_archive_path = rename_symbols(&custom_lib_target);

        if cfg!(feature = "embed-runtime") {
            // NOTE: lib, .a are added always on unix-like systems as described in:
            // https://gist.github.com/novafacing/1389cbb2f0a362d7eb103e67b4468e2b
            println!(
                "cargo:rustc-env=LIBAFL_LIBFUZZER_RUNTIME_PATH={}",
                redefined_archive_path.display()
            );
        }

        println!(
            "cargo:rustc-link-search=native={}",
            custom_lib_target.to_str().unwrap()
        );
        println!("cargo:rustc-link-lib=static=Fuzzer");

        if cfg!(target_os = "macos") {
            println!("cargo:rustc-link-lib=c++");
        } else {
            println!("cargo:rustc-link-lib=stdc++");
            println!("cargo:rustc-link-arg=-Wl,--allow-multiple-definition");
        }
        Ok(())
    }

    /// This function creates a copy of the libfuzzer runtime with all the symbols renamed to avoid
    /// a conflict with whatever we link to. It should be compatible with legacy and v0 mangling.
    fn rename_symbols(custom_lib_target: &Path) -> PathBuf {
        let mut archive_path = custom_lib_target.join(std::env::var_os("TARGET").unwrap());
        archive_path.push("release");

        archive_path.push("libafl_libfuzzer_runtime.a");
        let target_libdir = Command::new("rustc")
            .args(["--print", "target-libdir"])
            .output()
            .expect("Couldn't find rustc's target-libdir");
        let target_libdir = String::from_utf8(target_libdir.stdout).unwrap();
        let target_libdir = Path::new(target_libdir.trim());

        // NOTE: depends on llvm-tools
        let rust_objcopy = target_libdir.join("../bin/llvm-objcopy");
        let nm = target_libdir.join("../bin/llvm-nm");

        let nm = if nm.exists() {
            nm.to_str().unwrap().to_string()
        } else {
            libafl_build::find_llvm_tool("llvm-nm").expect("Could not find llvm-nm")
        };

        let rust_objcopy = if rust_objcopy.exists() {
            rust_objcopy.to_str().unwrap().to_string()
        } else {
            libafl_build::find_llvm_tool("llvm-objcopy").expect("Could not find llvm-objcopy")
        };

        let redefined_archive_path = custom_lib_target.join("libFuzzer.a");
        let redefined_symbols = custom_lib_target.join("redefs.txt");

        let mut nm_child = Command::new(nm)
        .arg(&archive_path)
        .stdout(Stdio::piped())
        .spawn()
        .expect("llvm-nm does not work (are you using nightly? or did you install by rustup component add llvm-tools?)");

        let mut redefinitions_file = BufWriter::new(File::create(&redefined_symbols).unwrap());

        let rn_prefix = if cfg!(target_os = "macos") {
            // macOS symbols have an extra `_`
            "__RN"
        } else {
            "_RN"
        };
        let r_prefix = if cfg!(target_os = "macos") {
            // macOS symbols have an extra `_`
            "__R"
        } else {
            "_R"
        };

        let zn_prefix = if cfg!(target_os = "macos") {
            // macOS symbols have an extra `_`
            "__ZN"
        } else {
            "_ZN"
        };

        let replacement = format!("{zn_prefix}{NAMESPACE_LEN}{NAMESPACE}");

        // redefine all the rust-mangled symbols we can
        for line in BufReader::new(nm_child.stdout.take().unwrap()).lines() {
            let line = line.unwrap();

            // Skip headers
            if line.ends_with(':') || line.is_empty() {
                continue;
            }
            let (_, symbol) = line.rsplit_once(' ').unwrap();

            if symbol.starts_with(rn_prefix) {
                if let Some((_prefix, renamed)) = symbol.split_once("__rustc") {
                    let (size, renamed) = renamed.split_once('_').unwrap();
                    writeln!(redefinitions_file, "{symbol} {replacement}{size}{renamed}E").unwrap();
                } else {
                    // v0 mangling: this uses the vendor-specific suffix model
                    writeln!(redefinitions_file, "{symbol} {symbol}${NAMESPACE}").unwrap();
                }
            } else if symbol.starts_with(zn_prefix) {
                writeln!(
                    redefinitions_file,
                    "{symbol} {}",
                    symbol.replacen(zn_prefix, &replacement, 1)
                )
                .unwrap();
            } else if symbol.starts_with(r_prefix) {
                // v0 mangling (not in namespace): this uses the vendor-specific suffix model
                writeln!(redefinitions_file, "{symbol} {symbol}${NAMESPACE}").unwrap();
            }
        }
        redefinitions_file.flush().unwrap();
        drop(redefinitions_file);

        assert!(
            nm_child.wait().is_ok_and(|s| s.success()),
            "Couldn't link runtime crate! Do you have the llvm-tools component installed? (`rustup component add llvm-tools-preview` to install)"
        );

        let mut objcopy_command = Command::new(rust_objcopy);

        for symbol in [
            "__rust_drop_panic",
            "__rust_foreign_exception",
            "rust_begin_unwind",
            "rust_panic",
            "rust_eh_personality",
            "__rg_oom",
            "__rdl_oom",
            "__rdl_alloc",
            "__rust_alloc",
            "__rdl_dealloc",
            "__rust_dealloc",
            "__rdl_realloc",
            "__rust_realloc",
            "__rdl_alloc_zeroed",
            "__rust_alloc_zeroed",
            "__rust_alloc_error_handler",
            "__rust_no_alloc_shim_is_unstable",
            "__rust_alloc_error_handler_should_panic",
        ] {
            let mut symbol = symbol.to_string();
            // macOS symbols have an extra `_`
            if cfg!(target_os = "macos") {
                symbol.insert(0, '_');
            }

            objcopy_command
                .arg("--redefine-sym")
                .arg(format!("{symbol}={symbol}_libafl_libfuzzer_runtime"));
        }

        objcopy_command
            .arg("--redefine-syms")
            .arg(redefined_symbols)
            .args([&archive_path, &redefined_archive_path]);

        assert!(
            objcopy_command.status().is_ok_and(|s| s.success()),
            "Couldn't rename allocators in the runtime crate! Do you have the llvm-tools component installed? (`rustup component add llvm-tools-preview` to install)"
        );
        redefined_archive_path
    }
}

fn main() {
    if std::env::var_os("DOCS_RS").is_some() || cfg!(any(clippy, docsrs)) {
        return; // skip when clippy or docs is running
    }

    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
    println!(
        "cargo:warning=The libafl_libfuzzer runtime may only be built for linux or macos; failing fast."
    );

    #[cfg(any(target_os = "linux", target_os = "macos"))]
    unix::build().expect("Failed to build libafl_libfuzzer runtime");
}