From 0cb64673e95cdd1f3aee5ea9493444e3709389fb Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Wed, 30 Sep 2026 15:03:24 +0100 Subject: [PATCH 1/5] Honor linker flavor flag --- cargo-auditable/src/rustc_arguments.rs | 38 ++++++++++++++------------ cargo-auditable/src/rustc_wrapper.rs | 25 +++++------------ cargo-auditable/src/target_info.rs | 18 ++++++++++-- 3 files changed, 44 insertions(+), 37 deletions(-) diff --git a/cargo-auditable/src/rustc_arguments.rs b/cargo-auditable/src/rustc_arguments.rs index 1e7b21a..31d954b 100644 --- a/cargo-auditable/src/rustc_arguments.rs +++ b/cargo-auditable/src/rustc_arguments.rs @@ -37,27 +37,31 @@ 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(!flavor.ends_with("cc")) + // 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 } } @@ -190,7 +194,7 @@ 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] @@ -207,6 +211,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..615bae4 100644 --- a/cargo-auditable/src/rustc_wrapper.rs +++ b/cargo-auditable/src/rustc_wrapper.rs @@ -7,12 +7,10 @@ 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, should_embed_audit_data, RustcArgs}, + target_info::{self, rustc_host_target_triple}, }; -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 +44,9 @@ 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(args: RustcArgs) -> bool { + args.bare_linker().unwrap_or(false) + // TODO: honor platform defaults } fn rustc_command_with_audit_data(rustc_path: &OsStr) -> Option { @@ -127,7 +116,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(args) { command.arg("-Clink-arg=-u"); command.arg("-Clink-arg=_AUDITABLE_VERSION_INFO"); } else { @@ -148,7 +137,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(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..a8ddbd7 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,20 @@ 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(|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!") +} + #[cfg(test)] mod tests { use super::*; From 37d23529f4809e1a433f561148d09325435c626d Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Wed, 30 Sep 2026 15:07:49 +0100 Subject: [PATCH 2/5] Add tests for linker flavor flag parsing --- cargo-auditable/src/rustc_arguments.rs | 46 ++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/cargo-auditable/src/rustc_arguments.rs b/cargo-auditable/src/rustc_arguments.rs index 31d954b..cae44d7 100644 --- a/cargo-auditable/src/rustc_arguments.rs +++ b/cargo-auditable/src/rustc_arguments.rs @@ -197,6 +197,52 @@ mod tests { 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"]; From f564ce8d84078b0e42bca6e95801d69aa58c3aff Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Wed, 30 Sep 2026 15:32:16 +0100 Subject: [PATCH 3/5] Add a function to query linker flavor from rustc where possible --- cargo-auditable/src/main.rs | 1 + cargo-auditable/src/target_json.rs | 66 ++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 cargo-auditable/src/target_json.rs 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/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 + ); + } + } +} From b1d1e7392c3f643997f40e7aae8efe85fc7adfc5 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Wed, 30 Sep 2026 15:43:59 +0100 Subject: [PATCH 4/5] Wire up detection of default linker flavor from the target platform JSON --- cargo-auditable/src/rustc_arguments.rs | 8 +++++++- cargo-auditable/src/rustc_wrapper.rs | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/cargo-auditable/src/rustc_arguments.rs b/cargo-auditable/src/rustc_arguments.rs index cae44d7..02a805b 100644 --- a/cargo-auditable/src/rustc_arguments.rs +++ b/cargo-auditable/src/rustc_arguments.rs @@ -54,7 +54,7 @@ impl RustcArgs { // flavor flag takes priority if let Some(flavor_flag) = flavor_flag { let flavor = flavor_flag.strip_prefix("linker-flavor=").unwrap(); - Some(!flavor.ends_with("cc")) + 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(); @@ -65,6 +65,12 @@ impl RustcArgs { } } +/// 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 { diff --git a/cargo-auditable/src/rustc_wrapper.rs b/cargo-auditable/src/rustc_wrapper.rs index 615bae4..50ad605 100644 --- a/cargo-auditable/src/rustc_wrapper.rs +++ b/cargo-auditable/src/rustc_wrapper.rs @@ -7,8 +7,9 @@ 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, RustcArgs}, + rustc_arguments::{self, linker_flavor_is_bare, should_embed_audit_data, RustcArgs}, target_info::{self, rustc_host_target_triple}, + target_json::rustc_target_json, }; pub fn main(rustc_path: &OsStr) { @@ -44,9 +45,14 @@ fn rustc_command(rustc_path: &OsStr) -> Command { command } -fn bare_linker(args: RustcArgs) -> bool { - args.bare_linker().unwrap_or(false) - // TODO: honor platform defaults +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 { @@ -116,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 bare_linker(args) { + if bare_linker(rustc_path, &target_triple, args) { command.arg("-Clink-arg=-u"); command.arg("-Clink-arg=_AUDITABLE_VERSION_INFO"); } else { @@ -137,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 bare_linker(args) { + if bare_linker(rustc_path, &target_triple, args) { command.arg("-Clink-arg=-u"); command.arg("-Clink-arg=AUDITABLE_VERSION_INFO"); } else { From d5109f2da0fbe3f493e320ad0d151431d7cf42a2 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Fri, 2 Oct 2026 13:34:33 +0100 Subject: [PATCH 5/5] Avoid obscure slice operation, use find_map instead --- cargo-auditable/src/target_info.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cargo-auditable/src/target_info.rs b/cargo-auditable/src/target_info.rs index a8ddbd7..4a76958 100644 --- a/cargo-auditable/src/target_info.rs +++ b/cargo-auditable/src/target_info.rs @@ -44,8 +44,7 @@ pub fn rustc_host_target_triple(rustc_path: &OsStr) -> String { .stdout .lines() .map(|l| l.unwrap()) - .find(|l| l.starts_with("host: ")) - .map(|l| l[6..].to_string()) + .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!") }