Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cargo-auditable/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod rustc_arguments;
mod rustc_wrapper;
mod sbom_precursor;
mod target_info;
mod target_json;

use std::process::exit;

Expand Down
90 changes: 73 additions & 17 deletions cargo-auditable/src/rustc_arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
/// <https://github.com/rust-secure-code/cargo-auditable/issues/202>
///
/// 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<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="));
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<OsString>) -> Result<RustcArgs, pico_args::Error> {
Expand Down Expand Up @@ -190,7 +200,53 @@ mod tests {
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());
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]
Expand All @@ -207,6 +263,6 @@ mod tests {

assert_eq!(args.codegen, expected);

assert!(!args.bare_linker());
assert!(!args.bare_linker().unwrap());
}
}
31 changes: 13 additions & 18 deletions cargo-auditable/src/rustc_wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Command> {
Expand Down Expand Up @@ -127,7 +122,7 @@ fn rustc_command_with_audit_data(rustc_path: &OsStr) -> Option<Command> {
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 {
Expand All @@ -148,7 +143,7 @@ fn rustc_command_with_audit_data(rustc_path: &OsStr) -> Option<Command> {
// 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 {
Expand Down
17 changes: 15 additions & 2 deletions cargo-auditable/src/target_info.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::{ffi::OsStr, io::BufRead};
use std::{ffi::OsStr, io::BufRead, process::Command};

pub type RustcTargetInfo = std::collections::HashMap<String, String>;

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()
Expand Down Expand Up @@ -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!")
}

@bjorn3 bjorn3 Sep 30, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rustc --print host-tuple is simpler, though I don't know if it was available on your MSRV already.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to support older rustc versions, so I aped this from Cargo and it never broke so I never changed it.


#[cfg(test)]
mod tests {
use super::*;
Expand Down
66 changes: 66 additions & 0 deletions cargo-auditable/src/target_json.rs
Original file line number Diff line number Diff line change
@@ -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<RustcTargetJson, Box<dyn Error>> {
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
);
}
}
}
Loading