diff --git a/cargo-auditable/src/main.rs b/cargo-auditable/src/main.rs index f4d88b7..0f811d2 100644 --- a/cargo-auditable/src/main.rs +++ b/cargo-auditable/src/main.rs @@ -11,6 +11,7 @@ mod rustc_arguments; mod rustc_wrapper; mod sbom_precursor; mod target_info; +mod target_json; use std::process::exit; diff --git a/cargo-auditable/src/rustc_arguments.rs b/cargo-auditable/src/rustc_arguments.rs index 1e7b21a..02a805b 100644 --- a/cargo-auditable/src/rustc_arguments.rs +++ b/cargo-auditable/src/rustc_arguments.rs @@ -37,30 +37,40 @@ impl RustcArgs { 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, - /// and the breakage it causes is subtle enough that people just roll with it - /// and complain when cargo-auditable doesn't support this configuration: - /// - /// - /// This function can tell you if a bare linker is in use - /// and whether you need to prepend `-Wl,` or not. + /// But it is possible to configure Cargo and rustc to call a linker directly. /// - /// Such setups are exceptionally rare and frankly it's a misconfiguration - /// that will break more than just `cargo auditable`, but I am feeling generous. - pub fn bare_linker(&self) -> bool { + /// This function does **not** take the target defaults into account. + pub fn bare_linker(&self) -> Option { + 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=")); - if let Some(linker_flag) = linker_flag { + // 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(); - if linker.ends_with("ld") { - return true; - } + Some(linker.ends_with("ld") || linker.ends_with("link")) + } else { + None } - false } } +/// 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) -> Result { @@ -190,7 +200,53 @@ mod tests { let raw_rustc_args = vec!["-C", "linker=rust-lld"]; let raw_rustc_args: Vec = raw_rustc_args.into_iter().map(|s| s.into()).collect(); let args = RustcArgs::from_vec(raw_rustc_args).unwrap(); - assert!(args.bare_linker()); + 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] @@ -207,6 +263,6 @@ mod tests { assert_eq!(args.codegen, expected); - assert!(!args.bare_linker()); + assert!(!args.bare_linker().unwrap()); } } diff --git a/cargo-auditable/src/rustc_wrapper.rs b/cargo-auditable/src/rustc_wrapper.rs index 0ba4755..50ad605 100644 --- a/cargo-auditable/src/rustc_wrapper.rs +++ b/cargo-auditable/src/rustc_wrapper.rs @@ -7,12 +7,11 @@ use std::{ use crate::{ binary_file, collect_audit_data, platform_detection::{is_32bit_x86, is_apple, is_msvc, is_wasm}, - rustc_arguments::{self, should_embed_audit_data}, - target_info, + rustc_arguments::{self, linker_flavor_is_bare, should_embed_audit_data, RustcArgs}, + target_info::{self, rustc_host_target_triple}, + target_json::rustc_target_json, }; -use std::io::BufRead; - pub fn main(rustc_path: &OsStr) { let mut command = match rustc_command_with_audit_data(rustc_path) { Some(cmd) => cmd, @@ -46,18 +45,14 @@ fn rustc_command(rustc_path: &OsStr) -> Command { command } -/// Returns the default target triple for the rustc we're running -fn rustc_host_target_triple(rustc_path: &OsStr) -> String { - Command::new(rustc_path) - .arg("-vV") - .output() - .expect("Failed to invoke rustc! Is it in your $PATH?") - .stdout - .lines() - .map(|l| l.unwrap()) - .find(|l| l.starts_with("host: ")) - .map(|l| l[6..].to_string()) - .expect("Failed to parse rustc output to determine the current platform. Please report this bug!") +fn bare_linker(rustc_path: &OsStr, target_triple: &str, args: RustcArgs) -> bool { + if let Some(explicit_arg) = args.bare_linker() { + explicit_arg + } else if let Ok(target_json) = rustc_target_json(rustc_path, target_triple) { + linker_flavor_is_bare(&target_json.linker_flavor) + } else { + false // bare linker configurations are rare + } } fn rustc_command_with_audit_data(rustc_path: &OsStr) -> Option { @@ -127,7 +122,7 @@ fn rustc_command_with_audit_data(rustc_path: &OsStr) -> Option { command.arg(linker_command); // Prevent the symbol from being removed as unused by the linker if is_apple(&target_info) { - if args.bare_linker() { + if bare_linker(rustc_path, &target_triple, args) { command.arg("-Clink-arg=-u"); command.arg("-Clink-arg=_AUDITABLE_VERSION_INFO"); } else { @@ -148,7 +143,7 @@ fn rustc_command_with_audit_data(rustc_path: &OsStr) -> Option { // Unrecognized platform, assume it to be unix-like. // Use POSIX `-u` instead of GNU `--undefined=` for broad compatibility // (e.g. zig rejects the GNU form). - if args.bare_linker() { + if bare_linker(rustc_path, &target_triple, args) { command.arg("-Clink-arg=-u"); command.arg("-Clink-arg=AUDITABLE_VERSION_INFO"); } else { diff --git a/cargo-auditable/src/target_info.rs b/cargo-auditable/src/target_info.rs index 23179ea..4a76958 100644 --- a/cargo-auditable/src/target_info.rs +++ b/cargo-auditable/src/target_info.rs @@ -1,10 +1,10 @@ -use std::{ffi::OsStr, io::BufRead}; +use std::{ffi::OsStr, io::BufRead, process::Command}; pub type RustcTargetInfo = std::collections::HashMap; pub fn rustc_target_info(rustc_path: &OsStr, target_triple: &str) -> RustcTargetInfo { // this is hand-rolled because the relevant piece of Cargo is hideously complex for some reason - parse_rustc_target_info(&std::process::Command::new(rustc_path) + parse_rustc_target_info(&Command::new(rustc_path) .arg("--print=cfg") .arg(format!("--target={target_triple}")) //not being parsed by the shell, so not a vulnerability .output() @@ -35,6 +35,19 @@ pub(crate) fn parse_rustc_target_info(rustc_output: &[u8]) -> RustcTargetInfo { .collect() } +/// Returns the default target triple for the rustc we're running +pub fn rustc_host_target_triple(rustc_path: &OsStr) -> String { + Command::new(rustc_path) + .arg("-vV") + .output() + .expect("Failed to invoke rustc! Is it in your $PATH?") + .stdout + .lines() + .map(|l| l.unwrap()) + .find_map(|l| l.strip_prefix("host: ").map(str::to_owned)) + .expect("Failed to parse rustc output to determine the current platform. Please report this bug!") +} + #[cfg(test)] mod tests { use super::*; diff --git a/cargo-auditable/src/target_json.rs b/cargo-auditable/src/target_json.rs new file mode 100644 index 0000000..9d72009 --- /dev/null +++ b/cargo-auditable/src/target_json.rs @@ -0,0 +1,66 @@ +use std::{error::Error, ffi::OsStr, process::Command}; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct RustcTargetJson { + #[serde(rename = "linker-flavor")] + pub linker_flavor: String, +} + +/// Queries target defaults, without applying command-line linker overrides. +/// +/// This function relies on nightly-only rustc functionality and may break. +/// This kind of linker target querying is really only needed for obscure +/// embedded platforms, and is not essential to the core functionality. +pub fn rustc_target_json( + rustc_path: &OsStr, + target_triple: &str, +) -> Result> { + let output = Command::new(rustc_path) + // Enable this unstable query only for the child process, including on stable rustc. + .env("RUSTC_BOOTSTRAP", "1") + .args(["-Z", "unstable-options", "--print", "target-spec-json"]) + .arg(format!("--target={target_triple}")) //not being parsed by the shell, so not a vulnerability + .output() + .map_err(|error| { + format!("Failed to invoke rustc to get target-spec-json for '{target_triple}': {error}") + })?; + + if !output.status.success() { + return Err(format!( + "rustc returned an error when asked for target-spec-json for '{target_triple}' ({}): {}", + output.status, + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } + + serde_json::from_slice(&output.stdout).map_err(|error| { + format!("Failed to parse rustc target specification for '{target_triple}': {error}").into() + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn query_linker_flavor() { + let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()); + for (target, expected_flavors) in [ + ( + "x86_64-unknown-linux-gnu", + &["gcc", "gnu-cc", "gnu-lld-cc"][..], + ), + ("thumbv7em-none-eabihf", &["ld.lld", "gnu-lld"][..]), + ] { + let spec = rustc_target_json(&rustc, target).unwrap(); + assert!( + expected_flavors.contains(&spec.linker_flavor.as_str()), + "unexpected linker flavor for {target}: {}", + spec.linker_flavor + ); + } + } +}