Skip to content
Open
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
26 changes: 26 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ anstyle = "1.0.11"
anyhow = "1.0.98"
base64 = "0.22.1"
blake3 = "1.8.2"
block2 = "0.6.1"
build-rs = { version = "0.3.1", path = "crates/build-rs" }
cargo = { path = "" }
cargo-credential = { version = "0.4.2", path = "credential/cargo-credential" }
Expand Down Expand Up @@ -69,6 +70,7 @@ libgit2-sys = "0.18.2"
libloading = "0.8.8"
memchr = "2.7.5"
miow = "0.6.0"
objc2 = "0.6.2"
Copy link
Contributor

Choose a reason for hiding this comment

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

This adds a dependency on the objc2 ecosystem (objc2, block2 and objc2-encode), which I maintain. They are fairly widely used, and Mozilla gfx-rs/wgpu#5641, though I acknowledge that this could still be seen as a supply-chain attack. Implementing the execution policy check without these crates is doable, though a lot more code.

opener = "0.8.2"
openssl = "0.10.73"
openssl-sys = "0.9.109"
Expand Down Expand Up @@ -253,6 +255,11 @@ features = [
"Win32_System_Threading",
]

# For ExecutionPolicy framework interaction.
[target.'cfg(target_vendor = "apple")'.dependencies]
block2.workspace = true
objc2.workspace = true

[dev-dependencies]
annotate-snippets = { workspace = true, features = ["testing-colors"] }
cargo-test-support.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion src/bin/cargo/commands/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {

let mut compile_opts = args.compile_options(
gctx,
UserIntent::Build,
UserIntent::Install,
workspace.as_ref(),
ProfileChecking::Custom,
)?;
Expand Down
24 changes: 22 additions & 2 deletions src/cargo/core/compiler/build_config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::core::compiler::CompileKind;
use crate::core::features::DetectAntivirus;
use crate::util::context::JobsConfig;
use crate::util::interning::InternedString;
use crate::util::{CargoResult, GlobalContext, RustfixDiagnosticServer};
Expand Down Expand Up @@ -52,6 +53,9 @@ pub struct BuildConfig {
pub sbom: bool,
/// Build compile time dependencies only, e.g., build scripts and proc macros
pub compile_time_deps_only: bool,
/// Whether we should try to detect and notify the user when antivirus
/// software might make newly created binaries slow to launch.
pub detect_antivirus: DetectAntivirus,
}

fn default_parallelism() -> CargoResult<u32> {
Expand Down Expand Up @@ -127,6 +131,19 @@ impl BuildConfig {
_ => Vec::new(),
};

let detect_antivirus = match (cfg.detect_antivirus, gctx.cli_unstable().detect_antivirus) {
// Warn while the config is still unstable.
(Some(_), DetectAntivirus::Never) => {
gctx.shell().warn(
"ignoring 'build.detect-antivirus' config, pass `-Zdetect-antivirus` to enable it",
)?;
DetectAntivirus::Never
}
// Allow overriding with config.
(Some(false), _) => DetectAntivirus::Never,
(_, flag) => flag,
};

Ok(BuildConfig {
requested_kinds,
jobs,
Expand All @@ -145,6 +162,7 @@ impl BuildConfig {
timing_outputs,
sbom,
compile_time_deps_only: false,
detect_antivirus,
})
}

Expand Down Expand Up @@ -280,12 +298,14 @@ impl CompileMode {
///
/// For example, when a user runs `cargo test`, the intent is [`UserIntent::Test`],
/// but this might result in multiple [`CompileMode`]s for different units.
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum UserIntent {
/// Build benchmark binaries, e.g., `cargo bench`
Bench,
/// Build binaries and libraries, e.g., `cargo run`, `cargo install`, `cargo build`.
/// Build binaries and libraries, e.g., `cargo run`, `cargo build`, `cargo rustc`.
Build,
/// Build binaries and libraries for installing, e.g. `cargo install`.
Install,
/// Perform type-check, e.g., `cargo check`.
Check { test: bool },
/// Document packages.
Expand Down
34 changes: 34 additions & 0 deletions src/cargo/core/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,38 @@ impl FromStr for FixEdition {
}
}

/// The value for `-Zdetect-antivirus`.
#[derive(Debug, Deserialize, Default, PartialEq, Eq, Hash, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum DetectAntivirus {
/// Always detect antivirus.
///
/// This is useful when testing the feature, but isn't expected to be
/// useful to the general user.
Always,
/// Detect antivirus, but only when deemed reasonable.
///
/// This is intended to be the default in the future.
Auto,
/// Never attempt to detect antivirus.
#[default]
Never,
}

impl FromStr for DetectAntivirus {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
Ok(match s {
"always" => Self::Always,
"auto" => Self::Auto,
"never" => Self::Never,
_ => bail!(
"invalid `-Zdetect-antivirus`, expected `always`, `auto` or `never`, got `{s}`"
),
})
}
}

#[derive(Debug, PartialEq)]
enum Status {
Stable,
Expand Down Expand Up @@ -854,6 +886,7 @@ unstable_cli_options!(
checksum_freshness: bool = ("Use a checksum to determine if output is fresh rather than filesystem mtime"),
codegen_backend: bool = ("Enable the `codegen-backend` option in profiles in .cargo/config.toml file"),
config_include: bool = ("Enable the `include` key in config files"),
detect_antivirus: DetectAntivirus = ("Enable the experimental antivirus detection and the config option to disable it"),
direct_minimal_versions: bool = ("Resolve minimal dependency versions instead of maximum (direct dependencies only)"),
dual_proc_macros: bool = ("Build proc-macros for both the host and the target"),
feature_unification: bool = ("Enable new feature unification modes in workspaces"),
Expand Down Expand Up @@ -1373,6 +1406,7 @@ impl CliUnstable {
"codegen-backend" => self.codegen_backend = parse_empty(k, v)?,
"config-include" => self.config_include = parse_empty(k, v)?,
"direct-minimal-versions" => self.direct_minimal_versions = parse_empty(k, v)?,
"detect-antivirus" => self.detect_antivirus = v.unwrap_or("auto").parse()?,
"dual-proc-macros" => self.dual_proc_macros = parse_empty(k, v)?,
"feature-unification" => self.feature_unification = parse_empty(k, v)?,
"fix-edition" => {
Expand Down
23 changes: 12 additions & 11 deletions src/cargo/ops/cargo_compile/compile_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,17 +219,18 @@ impl CompileFilter {
match intent {
UserIntent::Test | UserIntent::Doctest | UserIntent::Bench => true,
UserIntent::Check { test: true } => true,
UserIntent::Build | UserIntent::Doc { .. } | UserIntent::Check { test: false } => {
match *self {
CompileFilter::Default { .. } => false,
CompileFilter::Only {
ref examples,
ref tests,
ref benches,
..
} => examples.is_specific() || tests.is_specific() || benches.is_specific(),
}
}
UserIntent::Build
| UserIntent::Install
| UserIntent::Doc { .. }
| UserIntent::Check { test: false } => match *self {
CompileFilter::Default { .. } => false,
CompileFilter::Only {
ref examples,
ref tests,
ref benches,
..
} => examples.is_specific() || tests.is_specific() || benches.is_specific(),
},
}
}

Expand Down
45 changes: 42 additions & 3 deletions src/cargo/ops/cargo_compile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ use crate::core::compiler::UserIntent;
use crate::core::compiler::unit_dependencies::build_unit_dependencies;
use crate::core::compiler::unit_graph::{self, UnitDep, UnitGraph};
use crate::core::compiler::{BuildConfig, BuildContext, BuildRunner, Compilation};
use crate::core::compiler::{CompileKind, CompileTarget, RustcTargetData, Unit};
use crate::core::compiler::{CompileKind, CompileMode, CompileTarget, RustcTargetData, Unit};
use crate::core::compiler::{CrateType, TargetInfo, apply_env_config, standard_lib};
use crate::core::compiler::{DefaultExecutor, Executor, UnitInterner};
use crate::core::features::DetectAntivirus;
use crate::core::profiles::Profiles;
use crate::core::resolver::features::{self, CliFeatures, FeaturesFor};
use crate::core::resolver::{HasDevUnits, Resolve};
Expand All @@ -55,7 +56,7 @@ use crate::ops;
use crate::ops::resolve::{SpecsAndResolvedFeatures, WorkspaceResolve};
use crate::util::context::{GlobalContext, WarningHandling};
use crate::util::interning::InternedString;
use crate::util::{CargoResult, StableHasher};
use crate::util::{CargoResult, StableHasher, detect_antivirus};

mod compile_filter;
pub use compile_filter::{CompileFilter, FilterRule, LibRule};
Expand Down Expand Up @@ -228,7 +229,11 @@ pub fn create_bcx<'a, 'gctx>(

// Perform some pre-flight validation.
match build_config.intent {
UserIntent::Test | UserIntent::Build | UserIntent::Check { .. } | UserIntent::Bench => {
UserIntent::Test
| UserIntent::Build
| UserIntent::Install
| UserIntent::Check { .. }
| UserIntent::Bench => {
if ws.gctx().get_env("RUST_FLAGS").is_ok() {
gctx.shell().warn(
"Cargo does not read `RUST_FLAGS` environment variable. Did you mean `RUSTFLAGS`?",
Expand Down Expand Up @@ -556,6 +561,40 @@ where `<compatible-ver>` is the latest version supporting rustc {rustc_version}"
}
}

if build_config.detect_antivirus != DetectAntivirus::Never {
// Count the number of test binaries and build scripts we'll need to
// run. This doesn't take into account the binary that will be run
// if `cargo run` was specified, and doesn't handle pre-2024 `rustdoc`
// tests, but that's fine, this is only a heuristic.
let num_binaries = unit_graph
.keys()
.filter(|unit| {
matches!(
unit.mode,
CompileMode::Test | CompileMode::Doctest | CompileMode::RunCustomBuild
)
})
.count();

tracing::debug!("estimated {num_binaries} binaries that could be slowed down by antivirus");

// Heuristic: Only do the check if we have to run more than a specific
// number of binaries. This makes it so that small beginner projects
// don't hit this.
//
// We also don't want to do this check when installing, since there
// might be `cargo install` users who are not necessarily developers
// (and so the note will be irrelevant to them).
if (10 < num_binaries && build_config.intent != UserIntent::Install)
|| build_config.detect_antivirus == DetectAntivirus::Always
Comment on lines +588 to +589
Copy link
Contributor Author

@madsmtm madsmtm Sep 11, 2025

Choose a reason for hiding this comment

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

As an alternative, or additional thing, we could consider only doing the check once every X days, a bit similar to the automatic garbage collection.

This has the problem that it might take a long time for the user to notice the message, and worse, it makes Cargo's output non-deterministic, so I'm inclined not to do it.

{
if let Err(err) = detect_antivirus::detect_and_report(gctx) {
Copy link
Contributor

Choose a reason for hiding this comment

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

One thing I wanted to note is that there is a performance cost to this. At least on my system, an error result took over 60ms, and a passing result took 14ms. We try to keep cargo's overhead relatively small (I like to keep the budget under 500ms total). 14ms is probably ok, but it's inching towards the territory where it is significant.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, you're right.

On my machine (to have comparable numbers to those I've given elsewhere), I get:

Action Time
dlopen-ing ExecutionPolicy.framework 0.2-0.5ms
Create EPDeveloperTool ~0.3ms
Get authorization status (fail) 5-7ms
Get authorization status (success) 4-6ms
Check SIP status (fail) 2-4ms
Request access (fail) 8-22ms

This is of course unfortunate, and is more reason to not do the check unless the heuristic triggers (whatever the heuristic ends up being).

// Errors in this detection are not fatal.
tracing::error!("failed detecting whether binaries may be slow to run: {err}");
}
Comment on lines +591 to +594
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am a bit unsure whether the end of create_bcx is the right place for this check?

Copy link
Contributor

Choose a reason for hiding this comment

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

Any particular reason for the concern?

Note that we have another type of check in here, the rustc compatibility one which you put this right after so that works from a consolidating environment checks perspective.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's the right place, but I just wanted to call to attention that I'm not familiar with Cargo's internals, and that there might be a more suited place? But if you say it's correct, then it's correct.

}
}

let bcx = BuildContext::new(
ws,
pkg_set,
Expand Down
6 changes: 3 additions & 3 deletions src/cargo/ops/cargo_compile/unit_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ impl<'a> UnitGenerator<'a, '_> {
.iter()
.filter(|t| t.tested() || t.is_example())
.collect(),
UserIntent::Build | UserIntent::Check { .. } => targets
UserIntent::Build | UserIntent::Install | UserIntent::Check { .. } => targets
.iter()
.filter(|t| t.is_bin() || t.is_lib())
.collect(),
Expand Down Expand Up @@ -453,7 +453,7 @@ impl<'a> UnitGenerator<'a, '_> {
FilterRule::Just(_) => Target::is_test,
};
let test_mode = match self.intent {
UserIntent::Build => CompileMode::Test,
UserIntent::Build | UserIntent::Install => CompileMode::Test,
UserIntent::Check { .. } => CompileMode::Check { test: true },
_ => default_mode,
};
Expand Down Expand Up @@ -775,7 +775,7 @@ Rustdoc did not scrape the following examples because they require dev-dependenc
fn to_compile_mode(intent: UserIntent) -> CompileMode {
match intent {
UserIntent::Test | UserIntent::Bench => CompileMode::Test,
UserIntent::Build => CompileMode::Build,
UserIntent::Build | UserIntent::Install => CompileMode::Build,
UserIntent::Check { test } => CompileMode::Check { test },
UserIntent::Doc { .. } => CompileMode::Doc,
UserIntent::Doctest => CompileMode::Doctest,
Expand Down
2 changes: 2 additions & 0 deletions src/cargo/util/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2770,6 +2770,8 @@ pub struct CargoBuildConfig {
pub sbom: Option<bool>,
/// Unstable feature `-Zbuild-analysis`.
pub analysis: Option<CargoBuildAnalysis>,
/// Unstable feature `-Zdetect-antivirus`.
pub detect_antivirus: Option<bool>,
}

/// Metrics collection for build analysis.
Expand Down
Loading
Loading