cargo-auditable 0.7.7

Make production Rust binaries auditable
//! Parses rustc arguments to extract the info not provided via environment variables.

use std::{ffi::OsString, path::PathBuf};

// We use pico-args because we only need to extract a few specific arguments out of a larger set,
// and other parsers (rustc's `getopts`, cargo's `clap`) make that difficult.
//
// We also intentionally do very little validation, to avoid rejecting new configurations
// that may be added to rustc in the future.
//
// For reference, the rustc argument parsing code is at
// https://github.com/rust-lang/rust/blob/26ecd44160f54395b3bd5558cc5352f49cb0a0ba/compiler/rustc_session/src/config.rs

/// Includes only the rustc arguments we care about
#[derive(Debug)]
pub struct RustcArgs {
    pub crate_name: Option<String>,
    pub crate_types: Vec<String>,
    pub cfg: Vec<String>,
    pub emit: Vec<String>,
    pub out_dir: Option<PathBuf>,
    pub target: Option<String>,
    pub print: Vec<String>,
    pub codegen: Vec<String>,
}

impl RustcArgs {
    pub fn enabled_features(&self) -> Vec<&str> {
        let mut result = Vec::new();
        for item in &self.cfg {
            if item.starts_with("feature=\"") {
                // feature names cannot contain quotes according to the documentation:
                // https://doc.rust-lang.org/cargo/reference/features.html#the-features-section
                result.push(item.split('"').nth(1).unwrap());
            }
        }
        result
    }

    /// This function can tell you if a bare linker is in use based on rustc arguments.
    /// Returns None if it wasn't explicitly specified in the arguments.
    ///
    /// Normally `rustc` uses a C compiler such as `cc` or `clang` as linker,
    /// and arguments to the actual linker need to be passed prefixed with `-Wl,`.
    /// But it is possible to configure Cargo and rustc to call a linker directly.
    ///
    /// This function does **not** take the target defaults into account.
    pub fn bare_linker(&self) -> Option<bool> {
        let flavor_flag = self
            .codegen
            .iter()
            .find(|s| s.starts_with("linker-flavor="));
        let linker_flag = self.codegen.iter().find(|s| s.starts_with("linker="));
        // flavor flag takes priority
        if let Some(flavor_flag) = flavor_flag {
            let flavor = flavor_flag.strip_prefix("linker-flavor=").unwrap();
            Some(linker_flavor_is_bare(flavor))
        // if flavor is not passed explicitly, it is guessed from the linker
        } else if let Some(linker_flag) = linker_flag {
            let linker = linker_flag.strip_prefix("linker=").unwrap();
            Some(linker.ends_with("ld") || linker.ends_with("link"))
        } else {
            None
        }
    }
}

/// Checks if the specified linker flavor is bare (args passed directly to the linker)
/// or if we go through a C compiler first (and have to prefix linker args with -Wl)
pub fn linker_flavor_is_bare(flavor: &str) -> bool {
    !flavor.ends_with("cc")
}

impl RustcArgs {
    // Split into its own function for unit testing
    fn from_vec(raw_args: Vec<OsString>) -> Result<RustcArgs, pico_args::Error> {
        let mut parser = pico_args::Arguments::from_vec(raw_args);

        // --emit requires slightly more complex parsing
        let raw_emit_args: Vec<String> = parser.values_from_str("--emit")?;
        let mut emit: Vec<String> = Vec::new();
        for raw_arg in raw_emit_args {
            for item in raw_arg.split(',') {
                emit.push(item.to_owned());
            }
        }

        Ok(RustcArgs {
            crate_name: parser.opt_value_from_str("--crate-name")?,
            crate_types: parser.values_from_str("--crate-type")?,
            cfg: parser.values_from_str("--cfg")?,
            emit,
            out_dir: parser
                .opt_value_from_os_str::<&str, PathBuf, pico_args::Error>("--out-dir", |s| {
                    Ok(PathBuf::from(s))
                })?,
            target: parser.opt_value_from_str("--target")?,
            print: parser.values_from_str("--print")?,
            codegen: parser.values_from_str("-C")?,
        })
    }
}

pub fn parse_args() -> Result<RustcArgs, pico_args::Error> {
    let raw_args: Vec<OsString> = std::env::args_os().skip(2).collect();
    RustcArgs::from_vec(raw_args)
}

pub fn should_embed_audit_data(args: &RustcArgs) -> bool {
    // Only inject audit data into crate types 'bin' and 'cdylib',
    // it doesn't make sense for static libs and weird other types.
    if !(args.crate_types.contains(&"bin".to_owned())
        || args.crate_types.contains(&"cdylib".to_owned()))
    {
        return false;
    }

    // when --emit is specified explicitly, only inject audit data for --emit=link
    // because it doesn't make sense for all other types such as llvm-ir, asm, etc.
    if !args.emit.is_empty() && !args.emit.contains(&"link".to_owned()) {
        return false;
    }

    // --print disables compilation UNLESS --emit is also specified
    if !args.print.is_empty() && args.emit.is_empty() {
        return false;
    }

    true
}

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

    #[test]
    fn rustc_vv() {
        let raw_rustc_args = vec!["-vV"];
        let raw_rustc_args: Vec<OsString> = raw_rustc_args.into_iter().map(|s| s.into()).collect();
        let args = RustcArgs::from_vec(raw_rustc_args).unwrap();
        assert!(!should_embed_audit_data(&args));
    }

    #[test]
    fn rustc_version_verbose() {
        let raw_rustc_args = vec!["--version", "--verbose"];
        let raw_rustc_args: Vec<OsString> = raw_rustc_args.into_iter().map(|s| s.into()).collect();
        let args = RustcArgs::from_vec(raw_rustc_args).unwrap();
        assert!(!should_embed_audit_data(&args));
    }

    #[test]
    fn cargo_c_compatibility() {
        let raw_rustc_args = vec!["--crate-name", "rustls", "--edition=2021", "src/lib.rs", "--error-format=json", "--json=diagnostic-rendered-ansi,artifacts,future-incompat", "--crate-type", "staticlib", "--crate-type", "cdylib", "--emit=dep-info,link", "-C", "embed-bitcode=no", "-C", "debuginfo=2", "-C", "link-arg=-Wl,-soname,librustls.so.0.14.0", "-Cmetadata=rustls-ffi", "--cfg", "cargo_c", "--print", "native-static-libs", "--cfg", "feature=\"aws-lc-rs\"", "--cfg", "feature=\"capi\"", "--cfg", "feature=\"default\"", "--check-cfg", "cfg(docsrs)", "--check-cfg", "cfg(feature, values(\"aws-lc-rs\", \"capi\", \"cert_compression\", \"default\", \"no_log_capture\", \"read_buf\", \"ring\"))", "-C", "metadata=b6a43041f637feb8", "--out-dir", "/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps", "--target", "x86_64-unknown-linux-gnu", "-C", "linker=clang", "-C", "incremental=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/incremental", "-L", "dependency=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps", "-L", "dependency=/home/user/Code/rustls-ffi/target/debug/deps", "--extern", "libc=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/liblibc-4fc7c9f82dda33ee.rlib", "--extern", "log=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/liblog-6f7c8f4d1d5ec422.rlib", "--extern", "rustls=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/librustls-a93cda0ba0380929.rlib", "--extern", "pki_types=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/librustls_pki_types-27749859644f0979.rlib", "--extern", "rustls_platform_verifier=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/librustls_platform_verifier-bceca5cf09f3d7ba.rlib", "--extern", "webpki=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/deps/libwebpki-bc4a16dd84e0b062.rlib", "-C", "link-arg=-fuse-ld=/home/user/mold-2.32.0-x86_64-linux/bin/mold", "-L", "native=/home/user/Code/rustls-ffi/target/x86_64-unknown-linux-gnu/debug/build/aws-lc-sys-d52f8990d9ede41d/out"];
        let raw_rustc_args: Vec<OsString> = raw_rustc_args.into_iter().map(|s| s.into()).collect();
        let args = RustcArgs::from_vec(raw_rustc_args).unwrap();
        assert!(should_embed_audit_data(&args));
    }

    #[test]
    fn embed_licensing_compatibility() {
        // https://github.com/rust-secure-code/cargo-auditable/issues/198
        let raw_rustc_args = vec![
            "-",
            "--crate-name ___",
            "--print=file-names",
            "--crate-type bin",
            "--crate-type rlib",
            "--crate-type dylib",
            "--crate-type cdylib",
            "--crate-type staticlib",
            "--crate-type proc-macro",
            "--print=sysroot",
            "--print=split-debuginfo",
            "--print=crate-name",
            "--print=cfg",
        ];
        let raw_rustc_args: Vec<OsString> = raw_rustc_args.into_iter().map(|s| s.into()).collect();
        let args = RustcArgs::from_vec(raw_rustc_args).unwrap();
        assert!(!should_embed_audit_data(&args));
    }

    #[test]
    fn multiple_emit_values() {
        let raw_rustc_args = vec!["--emit=dep-info,link", "--emit", "llvm-bc"];
        let raw_rustc_args: Vec<OsString> = raw_rustc_args.into_iter().map(|s| s.into()).collect();
        let mut args = RustcArgs::from_vec(raw_rustc_args).unwrap();

        let expected = vec!["dep-info", "link", "llvm-bc"];
        let mut expected: Vec<String> = expected.into_iter().map(|s| s.into()).collect();

        args.emit.sort();
        expected.sort();

        assert_eq!(args.emit, expected)
    }

    #[test]
    fn detect_bare_linker() {
        let raw_rustc_args = vec!["-C", "linker=rust-lld"];
        let raw_rustc_args: Vec<OsString> = raw_rustc_args.into_iter().map(|s| s.into()).collect();
        let args = RustcArgs::from_vec(raw_rustc_args).unwrap();
        assert!(args.bare_linker().unwrap());
    }

    #[test]
    fn detect_bare_linker_from_flavor() {
        let cases = [
            ("gcc", false),
            ("gnu-cc", false),
            ("gnu-lld-cc", false),
            ("darwin-cc", false),
            ("darwin-lld-cc", false),
            ("wasm-lld-cc", false),
            ("unix-cc", false),
            ("ld", true),
            ("ld.lld", true),
            ("ld64.lld", true),
            ("lld-link", true),
            ("wasm-ld", true),
            ("msvc", true),
            ("gnu", true),
            ("gnu-lld", true),
            ("darwin", true),
            ("darwin-lld", true),
            ("wasm-lld", true),
            ("unix", true),
            ("msvc-lld", true),
            ("bpf", true),
            ("llbc", true),
            ("ptx", true),
        ];
        for (flavor, expected) in cases {
            // Both joined and separate forms of -C must behave the same way.
            for raw_args in [
                vec![OsString::from(format!("-Clinker-flavor={flavor}"))],
                vec!["-C".into(), format!("linker-flavor={flavor}").into()],
            ] {
                let args = RustcArgs::from_vec(raw_args).unwrap();
                assert_eq!(args.bare_linker(), Some(expected), "flavor: {flavor}");
            }
        }
    }

    #[test]
    fn bare_linker_unspecified() {
        let raw_args = vec!["-C".into(), "link-arg=-fuse-ld=lld".into()];
        let args = RustcArgs::from_vec(raw_args).unwrap();
        assert_eq!(args.bare_linker(), None);
    }

    #[test]
    fn multiple_codegen_options() {
        let raw_rustc_args = vec!["-Clinker=clang", "-C", "link-arg=-fuse-ld=/usr/bin/mold"];
        let raw_rustc_args: Vec<OsString> = raw_rustc_args.into_iter().map(|s| s.into()).collect();
        let mut args = RustcArgs::from_vec(raw_rustc_args).unwrap();

        let expected = vec!["linker=clang", "link-arg=-fuse-ld=/usr/bin/mold"];
        let mut expected: Vec<String> = expected.into_iter().map(|s| s.into()).collect();

        args.codegen.sort();
        expected.sort();

        assert_eq!(args.codegen, expected);

        assert!(!args.bare_linker().unwrap());
    }
}