From 04b38945a2a49e0418f6f0d5059725702277883f Mon Sep 17 00:00:00 2001 From: glaziermag Date: Sat, 24 Jan 2026 17:53:17 -0800 Subject: [PATCH 1/6] cargo-audit: add binary scan size limits Adds configurable limits for binary input size and auditable payload size to avoid OOM risks when scanning binaries. --- cargo-audit/audit.toml.example | 5 +++ cargo-audit/src/auditor.rs | 39 +++++++++++++++++-- cargo-audit/src/commands/audit.rs | 27 +++++++++++++ .../src/commands/audit/binary_scanning.rs | 16 ++++++++ cargo-audit/src/config.rs | 15 +++++++ 5 files changed, 99 insertions(+), 3 deletions(-) diff --git a/cargo-audit/audit.toml.example b/cargo-audit/audit.toml.example index c67e92a40..fea27e8e1 100644 --- a/cargo-audit/audit.toml.example +++ b/cargo-audit/audit.toml.example @@ -25,6 +25,11 @@ format = "terminal" # "terminal" (human readable report) or "json" quiet = false # Only print information on error show_tree = true # Show inverse dependency trees along with advisories (default: true) +# Binary scanning configuration +[binary] +max_binary_size = 104857600 # Max size of binary to read in bytes (default: unlimited) +audit_data_size_limit = 8388608 # Max size of embedded audit data in bytes (default: 8MB) + # Target Configuration [target] arch = ["x86_64"] # Ignore advisories for CPU architectures other than these diff --git a/cargo-audit/src/auditor.rs b/cargo-audit/src/auditor.rs index 4e07015ef..b8217f4bf 100644 --- a/cargo-audit/src/auditor.rs +++ b/cargo-audit/src/auditor.rs @@ -30,6 +30,10 @@ pub struct Auditor { /// Audit report settings report_settings: report::Settings, + + /// Binary scanning configuration + #[cfg(feature = "binary-scanning")] + binary: crate::config::BinaryConfig, } impl Auditor { @@ -179,6 +183,8 @@ impl Auditor { registry_index, presenter: Presenter::new(&config.output), report_settings: config.report_settings(), + #[cfg(feature = "binary-scanning")] + binary: config.binary.clone(), } } @@ -245,9 +251,11 @@ impl Auditor { /// Perform an audit of a binary file with dependency data embedded by `cargo auditable` fn audit_binary(&mut self, binary_path: &Path) -> rustsec::Result { use rustsec::binary_scanning::BinaryReport::*; - let file_contents = std::fs::read(binary_path)?; - let (binary_type, report) = - rustsec::binary_scanning::load_deps_from_binary(&file_contents, Option::None)?; + let file_contents = self.read_binary_with_limit(binary_path)?; + let (binary_type, report) = rustsec::binary_scanning::load_deps_from_binary( + &file_contents, + self.binary.audit_data_size_limit, + )?; self.presenter.binary_scan_report(&report, binary_path); match report { Complete(lockfile) | Incomplete(lockfile) => { @@ -260,6 +268,31 @@ impl Auditor { } } + #[cfg(feature = "binary-scanning")] + fn read_binary_with_limit(&self, binary_path: &Path) -> rustsec::Result> { + let mut file = std::fs::File::open(binary_path)?; + if let Some(limit) = self.binary.max_binary_size { + let mut limited = file.take(limit.saturating_add(1)); + let mut buffer = Vec::new(); + limited.read_to_end(&mut buffer)?; + if buffer.len() as u64 > limit { + return Err(Error::new( + ErrorKind::BadParam, + format!( + "binary {} exceeds max size limit of {} bytes", + binary_path.display(), + limit + ), + )); + } + Ok(buffer) + } else { + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer)?; + Ok(buffer) + } + } + /// The part of the auditing process that is shared between auditing lockfiles and binary files fn audit( &mut self, diff --git a/cargo-audit/src/commands/audit.rs b/cargo-audit/src/commands/audit.rs index dbe36b3e4..0473308af 100644 --- a/cargo-audit/src/commands/audit.rs +++ b/cargo-audit/src/commands/audit.rs @@ -222,6 +222,17 @@ impl Override for AuditCommand { config.target.os = Some(FilterList::Many(self.target_os.clone())); } + #[cfg(feature = "binary-scanning")] + if let Some(AuditSubcommand::Bin(bin)) = &self.subcommand { + if let Some(max_binary_size) = bin.max_binary_size { + config.binary.max_binary_size = Some(max_binary_size); + } + + if let Some(audit_data_size_limit) = bin.audit_data_size_limit { + config.binary.audit_data_size_limit = Some(audit_data_size_limit); + } + } + if let Some(url) = &self.url { config.database.url = Some(url.clone()) } @@ -321,4 +332,20 @@ mod tests { let overridden_config = audit_command.override_config(config.clone()).unwrap(); assert!(!overridden_config.database.fetch); } + + #[cfg(feature = "binary-scanning")] + #[test] + fn override_binary_limits_from_cli() { + let config: AuditConfig = AuditConfig::default(); + let mut audit_command = AuditCommand::default(); + + let mut bin = BinCommand::default(); + bin.max_binary_size = Some(1024); + bin.audit_data_size_limit = Some(2048); + audit_command.subcommand = Some(AuditSubcommand::Bin(bin)); + + let overridden_config = audit_command.override_config(config).unwrap(); + assert_eq!(overridden_config.binary.max_binary_size, Some(1024)); + assert_eq!(overridden_config.binary.audit_data_size_limit, Some(2048)); + } } diff --git a/cargo-audit/src/commands/audit/binary_scanning.rs b/cargo-audit/src/commands/audit/binary_scanning.rs index 9f791f996..e1334391c 100644 --- a/cargo-audit/src/commands/audit/binary_scanning.rs +++ b/cargo-audit/src/commands/audit/binary_scanning.rs @@ -9,6 +9,22 @@ use std::{path::PathBuf, process::exit}; #[derive(Command, Clone, Default, Debug, Parser)] #[command()] pub struct BinCommand { + /// Maximum binary size in bytes to read + #[arg( + long = "max-binary-size", + value_name = "BYTES", + help = "Maximum binary size in bytes to read" + )] + pub(super) max_binary_size: Option, + + /// Maximum audit data size in bytes to parse + #[arg( + long = "audit-data-size-limit", + value_name = "BYTES", + help = "Maximum audit data size in bytes to parse (default: 8MB)" + )] + pub(super) audit_data_size_limit: Option, + /// Paths to the binaries to be scanned #[arg( value_parser, diff --git a/cargo-audit/src/config.rs b/cargo-audit/src/config.rs index 700a6e7db..f3774bd12 100644 --- a/cargo-audit/src/config.rs +++ b/cargo-audit/src/config.rs @@ -27,6 +27,10 @@ pub struct AuditConfig { #[serde(default)] pub output: OutputConfig, + /// Binary scanning configuration + #[serde(default)] + pub binary: BinaryConfig, + /// Target-related configuration #[serde(default)] pub target: TargetConfig, @@ -168,6 +172,17 @@ impl OutputConfig { } } +/// Binary scanning configuration +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct BinaryConfig { + /// Maximum binary size in bytes to read (no limit if unset) + pub max_binary_size: Option, + + /// Maximum audit data size in bytes to parse (defaults to 8MB if unset) + pub audit_data_size_limit: Option, +} + /// Warning kinds #[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize, Ord)] pub enum DenyOption { From 5bd48a802e27e9532e2d10c1f7baedb79fd7a249 Mon Sep 17 00:00:00 2001 From: glaziermag Date: Sat, 24 Jan 2026 18:28:20 -0800 Subject: [PATCH 2/6] ci: trigger actions From 2f72745240718e570001df4610afa92bd9c93235 Mon Sep 17 00:00:00 2001 From: glaziermag Date: Sat, 24 Jan 2026 18:41:28 -0800 Subject: [PATCH 3/6] cargo-audit: configure bin scan limits via CLI Avoids a semver-breaking change to AuditConfig by applying binary scan limits directly on the auditor. Adds a default 100MB input cap (0 disables) and supports overriding the auditable payload limit. --- cargo-audit/audit.toml.example | 5 -- cargo-audit/src/auditor.rs | 84 ++++++++++++++----- cargo-audit/src/commands/audit.rs | 26 ------ .../src/commands/audit/binary_scanning.rs | 10 ++- cargo-audit/src/config.rs | 15 ---- 5 files changed, 68 insertions(+), 72 deletions(-) diff --git a/cargo-audit/audit.toml.example b/cargo-audit/audit.toml.example index fea27e8e1..c67e92a40 100644 --- a/cargo-audit/audit.toml.example +++ b/cargo-audit/audit.toml.example @@ -25,11 +25,6 @@ format = "terminal" # "terminal" (human readable report) or "json" quiet = false # Only print information on error show_tree = true # Show inverse dependency trees along with advisories (default: true) -# Binary scanning configuration -[binary] -max_binary_size = 104857600 # Max size of binary to read in bytes (default: unlimited) -audit_data_size_limit = 8388608 # Max size of embedded audit data in bytes (default: 8MB) - # Target Configuration [target] arch = ["x86_64"] # Ignore advisories for CPU architectures other than these diff --git a/cargo-audit/src/auditor.rs b/cargo-audit/src/auditor.rs index b8217f4bf..6ac772fa1 100644 --- a/cargo-audit/src/auditor.rs +++ b/cargo-audit/src/auditor.rs @@ -17,6 +17,16 @@ use std::{ // TODO: make configurable const DEFAULT_LOCK_TIMEOUT: Duration = Duration::from_secs(5 * 60); +#[cfg(feature = "binary-scanning")] +const DEFAULT_MAX_BINARY_SIZE: u64 = 100 * 1024 * 1024; // 100MB + +#[cfg(feature = "binary-scanning")] +#[derive(Clone, Copy, Debug)] +enum BinarySizeLimit { + Unlimited, + Max(u64), +} + /// Security vulnerability auditor pub struct Auditor { /// RustSec Advisory Database @@ -31,9 +41,13 @@ pub struct Auditor { /// Audit report settings report_settings: report::Settings, - /// Binary scanning configuration + /// Binary scanning configuration (max input size) + #[cfg(feature = "binary-scanning")] + binary_size_limit: BinarySizeLimit, + + /// Binary scanning configuration (max auditable payload size) #[cfg(feature = "binary-scanning")] - binary: crate::config::BinaryConfig, + audit_data_size_limit: Option, } impl Auditor { @@ -184,7 +198,9 @@ impl Auditor { presenter: Presenter::new(&config.output), report_settings: config.report_settings(), #[cfg(feature = "binary-scanning")] - binary: config.binary.clone(), + binary_size_limit: BinarySizeLimit::Max(DEFAULT_MAX_BINARY_SIZE), + #[cfg(feature = "binary-scanning")] + audit_data_size_limit: None, } } @@ -247,6 +263,27 @@ impl Auditor { summary } + #[cfg(feature = "binary-scanning")] + /// Configure binary-scanning limits for this `Auditor`. + /// + /// `max_binary_size` is in bytes. If unset, defaults to 100MB. If set to `0`, disables the + /// limit. + /// + /// `audit_data_size_limit` is the maximum size (in bytes) of embedded auditable payload data + /// to parse. If unset, the default from `rustsec` applies (currently 8MB). + pub fn set_binary_scan_limits( + &mut self, + max_binary_size: Option, + audit_data_size_limit: Option, + ) { + self.binary_size_limit = match max_binary_size { + Some(0) => BinarySizeLimit::Unlimited, + Some(n) => BinarySizeLimit::Max(n), + None => BinarySizeLimit::Max(DEFAULT_MAX_BINARY_SIZE), + }; + self.audit_data_size_limit = audit_data_size_limit; + } + #[cfg(feature = "binary-scanning")] /// Perform an audit of a binary file with dependency data embedded by `cargo auditable` fn audit_binary(&mut self, binary_path: &Path) -> rustsec::Result { @@ -254,7 +291,7 @@ impl Auditor { let file_contents = self.read_binary_with_limit(binary_path)?; let (binary_type, report) = rustsec::binary_scanning::load_deps_from_binary( &file_contents, - self.binary.audit_data_size_limit, + self.audit_data_size_limit, )?; self.presenter.binary_scan_report(&report, binary_path); match report { @@ -271,25 +308,28 @@ impl Auditor { #[cfg(feature = "binary-scanning")] fn read_binary_with_limit(&self, binary_path: &Path) -> rustsec::Result> { let mut file = std::fs::File::open(binary_path)?; - if let Some(limit) = self.binary.max_binary_size { - let mut limited = file.take(limit.saturating_add(1)); - let mut buffer = Vec::new(); - limited.read_to_end(&mut buffer)?; - if buffer.len() as u64 > limit { - return Err(Error::new( - ErrorKind::BadParam, - format!( - "binary {} exceeds max size limit of {} bytes", - binary_path.display(), - limit - ), - )); + match self.binary_size_limit { + BinarySizeLimit::Unlimited => { + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer)?; + Ok(buffer) + } + BinarySizeLimit::Max(limit) => { + let mut limited = file.take(limit.saturating_add(1)); + let mut buffer = Vec::new(); + limited.read_to_end(&mut buffer)?; + if buffer.len() as u64 > limit { + return Err(Error::new( + ErrorKind::BadParam, + format!( + "binary {} exceeds max size limit of {} bytes", + binary_path.display(), + limit + ), + )); + } + Ok(buffer) } - Ok(buffer) - } else { - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer)?; - Ok(buffer) } } diff --git a/cargo-audit/src/commands/audit.rs b/cargo-audit/src/commands/audit.rs index 0473308af..d51f7d090 100644 --- a/cargo-audit/src/commands/audit.rs +++ b/cargo-audit/src/commands/audit.rs @@ -222,17 +222,6 @@ impl Override for AuditCommand { config.target.os = Some(FilterList::Many(self.target_os.clone())); } - #[cfg(feature = "binary-scanning")] - if let Some(AuditSubcommand::Bin(bin)) = &self.subcommand { - if let Some(max_binary_size) = bin.max_binary_size { - config.binary.max_binary_size = Some(max_binary_size); - } - - if let Some(audit_data_size_limit) = bin.audit_data_size_limit { - config.binary.audit_data_size_limit = Some(audit_data_size_limit); - } - } - if let Some(url) = &self.url { config.database.url = Some(url.clone()) } @@ -333,19 +322,4 @@ mod tests { assert!(!overridden_config.database.fetch); } - #[cfg(feature = "binary-scanning")] - #[test] - fn override_binary_limits_from_cli() { - let config: AuditConfig = AuditConfig::default(); - let mut audit_command = AuditCommand::default(); - - let mut bin = BinCommand::default(); - bin.max_binary_size = Some(1024); - bin.audit_data_size_limit = Some(2048); - audit_command.subcommand = Some(AuditSubcommand::Bin(bin)); - - let overridden_config = audit_command.override_config(config).unwrap(); - assert_eq!(overridden_config.binary.max_binary_size, Some(1024)); - assert_eq!(overridden_config.binary.audit_data_size_limit, Some(2048)); - } } diff --git a/cargo-audit/src/commands/audit/binary_scanning.rs b/cargo-audit/src/commands/audit/binary_scanning.rs index e1334391c..5d91456c9 100644 --- a/cargo-audit/src/commands/audit/binary_scanning.rs +++ b/cargo-audit/src/commands/audit/binary_scanning.rs @@ -13,9 +13,9 @@ pub struct BinCommand { #[arg( long = "max-binary-size", value_name = "BYTES", - help = "Maximum binary size in bytes to read" + help = "Maximum binary size in bytes to read (default: 100MB; use 0 for unlimited)" )] - pub(super) max_binary_size: Option, + max_binary_size: Option, /// Maximum audit data size in bytes to parse #[arg( @@ -23,7 +23,7 @@ pub struct BinCommand { value_name = "BYTES", help = "Maximum audit data size in bytes to parse (default: 8MB)" )] - pub(super) audit_data_size_limit: Option, + audit_data_size_limit: Option, /// Paths to the binaries to be scanned #[arg( @@ -36,7 +36,9 @@ pub struct BinCommand { impl Runnable for BinCommand { fn run(&self) { - let report = self.auditor().audit_binaries(&self.binary_paths); + let mut auditor = self.auditor(); + auditor.set_binary_scan_limits(self.max_binary_size, self.audit_data_size_limit); + let report = auditor.audit_binaries(&self.binary_paths); if report.vulnerabilities_found { exit(1) } else if report.errors_encountered { diff --git a/cargo-audit/src/config.rs b/cargo-audit/src/config.rs index f3774bd12..700a6e7db 100644 --- a/cargo-audit/src/config.rs +++ b/cargo-audit/src/config.rs @@ -27,10 +27,6 @@ pub struct AuditConfig { #[serde(default)] pub output: OutputConfig, - /// Binary scanning configuration - #[serde(default)] - pub binary: BinaryConfig, - /// Target-related configuration #[serde(default)] pub target: TargetConfig, @@ -172,17 +168,6 @@ impl OutputConfig { } } -/// Binary scanning configuration -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[serde(deny_unknown_fields)] -pub struct BinaryConfig { - /// Maximum binary size in bytes to read (no limit if unset) - pub max_binary_size: Option, - - /// Maximum audit data size in bytes to parse (defaults to 8MB if unset) - pub audit_data_size_limit: Option, -} - /// Warning kinds #[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize, Ord)] pub enum DenyOption { From 33dd4b366f0edd69d374bd804499db899371b949 Mon Sep 17 00:00:00 2001 From: glaziermag Date: Sat, 24 Jan 2026 18:42:36 -0800 Subject: [PATCH 4/6] rustfmt --- cargo-audit/src/commands/audit.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/cargo-audit/src/commands/audit.rs b/cargo-audit/src/commands/audit.rs index d51f7d090..dbe36b3e4 100644 --- a/cargo-audit/src/commands/audit.rs +++ b/cargo-audit/src/commands/audit.rs @@ -321,5 +321,4 @@ mod tests { let overridden_config = audit_command.override_config(config.clone()).unwrap(); assert!(!overridden_config.database.fetch); } - } From cd04de769c3e18f9778a169abf5601611d97831e Mon Sep 17 00:00:00 2001 From: glaziermag Date: Sun, 25 Jan 2026 05:50:18 -0800 Subject: [PATCH 5/6] cargo-audit: test max binary size limit Add a regression test which ensures `cargo audit bin --max-binary-size` rejects oversized inputs. --- cargo-audit/tests/binary_scanning.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/cargo-audit/tests/binary_scanning.rs b/cargo-audit/tests/binary_scanning.rs index 1d820e0d8..ec89c800b 100644 --- a/cargo-audit/tests/binary_scanning.rs +++ b/cargo-audit/tests/binary_scanning.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use abscissa_core::testing::prelude::*; use once_cell::sync::Lazy; use tempfile::TempDir; +use std::{fs, io::Read}; /// Directory containing the advisory database. /// @@ -39,6 +40,28 @@ fn cmd_runner() -> CmdRunner { RUNNER.clone() } +#[test] +fn oversized_binary_is_rejected() { + let tmpdir = TempDir::new().unwrap(); + let binary_path = tmpdir.path().join("not-a-real-binary"); + + // Ensure the file is larger than the configured max size. + fs::write(&binary_path, b"0123").unwrap(); + + let mut runner = cmd_runner(); + runner + .arg(&binary_path) + .arg("--max-binary-size") + .arg("1") + .capture_stderr(); + + let mut process = runner.run(); + let mut stderr = String::new(); + process.stderr().read_to_string(&mut stderr).unwrap(); + process.wait().unwrap().expect_code(2); + assert!(stderr.contains("exceeds max size limit of 1 bytes"), "{stderr}"); +} + #[test] fn panicking_binary_without_vulnerabilities_passes() { let mut binary_path = binaries_dir(); From e352ad8db530a9af95f801430107153c200d08e1 Mon Sep 17 00:00:00 2001 From: glaziermag Date: Sun, 25 Jan 2026 05:51:31 -0800 Subject: [PATCH 6/6] rustfmt --- cargo-audit/tests/binary_scanning.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cargo-audit/tests/binary_scanning.rs b/cargo-audit/tests/binary_scanning.rs index ec89c800b..7a4806cd5 100644 --- a/cargo-audit/tests/binary_scanning.rs +++ b/cargo-audit/tests/binary_scanning.rs @@ -4,8 +4,8 @@ use std::path::PathBuf; use abscissa_core::testing::prelude::*; use once_cell::sync::Lazy; -use tempfile::TempDir; use std::{fs, io::Read}; +use tempfile::TempDir; /// Directory containing the advisory database. /// @@ -59,7 +59,10 @@ fn oversized_binary_is_rejected() { let mut stderr = String::new(); process.stderr().read_to_string(&mut stderr).unwrap(); process.wait().unwrap().expect_code(2); - assert!(stderr.contains("exceeds max size limit of 1 bytes"), "{stderr}"); + assert!( + stderr.contains("exceeds max size limit of 1 bytes"), + "{stderr}" + ); } #[test]