Skip to content

Commit 332db70

Browse files
committed
feat: Implement antivirus detection on macOS
Having this enabled will make every initial binary launch slower. We check this by seeing if we have the "Developer Tool" execution policy grant, or if SIP Filesystem Protections are disabled.
1 parent e24f3ff commit 332db70

File tree

7 files changed

+416
-1
lines changed

7 files changed

+416
-1
lines changed

Cargo.lock

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ anstyle = "1.0.11"
2424
anyhow = "1.0.98"
2525
base64 = "0.22.1"
2626
blake3 = "1.8.2"
27+
block2 = "0.6.1"
2728
build-rs = { version = "0.3.1", path = "crates/build-rs" }
2829
cargo = { path = "" }
2930
cargo-credential = { version = "0.4.2", path = "credential/cargo-credential" }
@@ -69,6 +70,7 @@ libgit2-sys = "0.18.2"
6970
libloading = "0.8.8"
7071
memchr = "2.7.5"
7172
miow = "0.6.0"
73+
objc2 = "0.6.2"
7274
opener = "0.8.2"
7375
openssl = "0.10.73"
7476
openssl-sys = "0.9.109"
@@ -251,6 +253,11 @@ features = [
251253
"Win32_System_Threading",
252254
]
253255

256+
[target.'cfg(target_vendor = "apple")'.dependencies]
257+
block2.workspace = true # For ExecutionPolicy framework interaction
258+
objc2.workspace = true # For ExecutionPolicy framework interaction
259+
libc.workspace = true
260+
254261
[dev-dependencies]
255262
annotate-snippets = { workspace = true, features = ["testing-colors"] }
256263
cargo-test-support.workspace = true

src/cargo/ops/cargo_compile/mod.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ use crate::ops;
5555
use crate::ops::resolve::{SpecsAndResolvedFeatures, WorkspaceResolve};
5656
use crate::util::context::{GlobalContext, WarningHandling};
5757
use crate::util::interning::InternedString;
58-
use crate::util::{CargoResult, StableHasher};
58+
use crate::util::{CargoResult, StableHasher, detect_antivirus};
5959

6060
mod compile_filter;
6161
pub use compile_filter::{CompileFilter, FilterRule, LibRule};
@@ -556,6 +556,22 @@ where `<compatible-ver>` is the latest version supporting rustc {rustc_version}"
556556
}
557557
}
558558

559+
// TODO(madsmtm): Add some sort of option for this.
560+
if false {
561+
// TODO(madsmtm): Maybe only do this when we have above a certain
562+
// number of build scripts or test binaries to run?
563+
564+
// TODO(madsmtm): We probably don't want to do this check when doing
565+
// `cargo install`?
566+
567+
// TODO(madsmtm): Consider only warning once every X days.
568+
569+
if let Err(err) = detect_antivirus::detect_and_report(gctx) {
570+
// Errors in this detection are not fatal.
571+
tracing::error!("failed detecting whether binaries may be slow to run: {err}");
572+
}
573+
}
574+
559575
let bcx = BuildContext::new(
560576
ws,
561577
pkg_set,
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
//! Utilities for using a dynamically loaded ExecutionPolicy.framework.
2+
//!
3+
//! ExecutionPolicy is only available since macOS 10.15, while Rust's
4+
//! minimum supported version for host tooling is macOS 10.12:
5+
//! https://doc.rust-lang.org/rustc/platform-support/apple-darwin.html#host-tooling
6+
//!
7+
//! For this reason, we must load the framework dynamically instead of linking
8+
//! it statically - which gets a bit more involved.
9+
//!
10+
//! See <https://docs.rs/objc2-execution-policy> for a safer interface that
11+
//! can be used if support for lower macOS versions are dropped (or once Rust
12+
//! gains better support for weak linking).
13+
//!
14+
//! NOTE: `addPolicyExceptionForURL:error:` probably isn't relevant for us,
15+
//! that is more used for e.g. allowing running a recently downloaded binary
16+
//! (and requires that you already have developer tool authorization).
17+
18+
use std::cell::Cell;
19+
use std::ffi::{CStr, c_void};
20+
use std::marker::PhantomData;
21+
use std::rc::Rc;
22+
23+
use anyhow::Context;
24+
use block2::{DynBlock, RcBlock};
25+
use objc2::ffi::NSInteger;
26+
use objc2::rc::Retained;
27+
use objc2::runtime::{AnyClass, Bool, NSObject};
28+
use objc2::{available, msg_send};
29+
30+
use crate::CargoResult;
31+
32+
/// A handle to the dynamically loaded ExecutionPolicy framework.
33+
#[derive(Debug)]
34+
pub struct ExecutionPolicyHandle(*mut c_void);
35+
36+
impl ExecutionPolicyHandle {
37+
/// Dynamically load the ExecutionPolicy framework, and return None if it
38+
/// isn't available.
39+
pub fn open() -> CargoResult<Option<Self>> {
40+
let path = c"/System/Library/Frameworks/ExecutionPolicy.framework/ExecutionPolicy";
41+
42+
let handle = unsafe { libc::dlopen(path.as_ptr(), libc::RTLD_LAZY | libc::RTLD_LOCAL) };
43+
44+
if handle.is_null() {
45+
// SAFETY: `dlerror` is safe to call.
46+
let err = unsafe { libc::dlerror() };
47+
let err = if err.is_null() {
48+
None
49+
} else {
50+
// SAFETY: The error is a valid C string.
51+
Some(unsafe { CStr::from_ptr(err) })
52+
};
53+
54+
// The framework was introduced in macOS 10.15+ / Mac Catalyst 13.0+.
55+
if available!(macos = 10.15, ios = 13.0) {
56+
Err(anyhow::format_err!(
57+
"failed loading ExecutionPolicy.framework: {err:?}"
58+
))
59+
} else {
60+
// The framework is not available on macOS 10.14 and below
61+
// (which also means that the antivirus doesn't exist yet, so
62+
// nothing for us to detect and warn against).
63+
Ok(None)
64+
}
65+
} else {
66+
Ok(Some(Self(handle)))
67+
}
68+
}
69+
}
70+
71+
impl Drop for ExecutionPolicyHandle {
72+
fn drop(&mut self) {
73+
// SAFETY: The handle is valid.
74+
let _ = unsafe { libc::dlclose(self.0) };
75+
// Ignore errors when closing. This is also what `libloading` does:
76+
// https://docs.rs/libloading/0.8.6/src/libloading/os/unix/mod.rs.html#374
77+
}
78+
}
79+
80+
/// A that interacts with the system via XPC.
81+
///
82+
/// See [`objc2_execution_policy::EPDeveloperTool`] for details.
83+
///
84+
/// [`objc2_execution_policy::EPDeveloperTool`]: https://docs.rs/objc2-execution-policy/0.3.1/objc2_execution_policy/struct.EPDeveloperTool.html
85+
#[derive(Debug)]
86+
pub struct EPDeveloperTool<'handle> {
87+
_handle: PhantomData<&'handle ExecutionPolicyHandle>,
88+
obj: Retained<NSObject>,
89+
}
90+
91+
impl<'handle> EPDeveloperTool<'handle> {
92+
/// Call `+[EPDeveloperTool new]` to get a new handle.
93+
pub fn new(_handle: &'handle ExecutionPolicyHandle) -> CargoResult<Self> {
94+
// Dynamically query the class (loading the framework with dlopen
95+
// above should have made this available).
96+
let cls =
97+
AnyClass::get(c"EPDeveloperTool").context("failed finding `EPDeveloperTool` class")?;
98+
99+
// SAFETY: The signature of +[EPDeveloperTool new] is correct and
100+
// the method is safe to call.
101+
let obj: Option<Retained<NSObject>> = unsafe { msg_send![cls, new] };
102+
103+
// Null can happen in OOM situations, and maybe if failing to connect
104+
// via. XPC to the required services.
105+
let obj = obj.context("failed allocating and initializing `EPDeveloperTool` instance")?;
106+
107+
let _handle = PhantomData;
108+
Ok(Self { _handle, obj })
109+
}
110+
111+
/// Call `-[EPDeveloperTool authorizationStatus]`.
112+
pub fn authorization_status(&self) -> EPDeveloperToolStatus {
113+
// SAFETY: -[EPDeveloperTool authorizationStatus] correctly
114+
// returns EPDeveloperToolStatus and the method is safe to call.
115+
let status: NSInteger = unsafe { msg_send![&*self.obj, authorizationStatus] };
116+
EPDeveloperToolStatus(status)
117+
}
118+
119+
/// Call `requestDeveloperToolAccessWithCompletionHandler:` and get the
120+
/// result.
121+
////
122+
/// This allows the user to more easily see which application needs to be
123+
/// allowed (but _is_ also requesting higher privileges, so we need to be
124+
/// clear in messaging around that).
125+
pub fn request_access(&self) -> CargoResult<bool> {
126+
// Wrapper to make the signature easier to write.
127+
fn inner(obj: &NSObject, block: &DynBlock<dyn Fn(Bool) + 'static>) {
128+
// SAFETY:
129+
// - The method is safe to call, and we provide a correctly typed
130+
// block, and constrain the signature to be void / unit return.
131+
// - No Send/Sync requirements are needed, because the block is
132+
// not marked @Sendable in Swift.
133+
// - The 'static requirement on the block is needed because the
134+
// block is marked as @escaping in Swift. Note that the fact
135+
// that the API is annotated as such is kind of weird, there
136+
// isn't really a way that it could call this block on the
137+
// current thread later (which is what a lone @escaping means).
138+
unsafe { msg_send![obj, requestDeveloperToolAccessWithCompletionHandler: block] }
139+
}
140+
141+
let result = Rc::new(Cell::new(None));
142+
let result_clone = result.clone();
143+
let block = RcBlock::new(move |granted: Bool| result_clone.set(Some(granted.as_bool())));
144+
inner(&self.obj, &block);
145+
result.get().context("failed getting result of -[EPDeveloperTool requestDeveloperToolAccessWithCompletionHandler:]")
146+
}
147+
}
148+
149+
/// The Developer Tool status of the process.
150+
#[repr(transparent)]
151+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
152+
pub struct EPDeveloperToolStatus(pub NSInteger);
153+
154+
impl EPDeveloperToolStatus {
155+
#[doc(alias = "EPDeveloperToolStatusNotDetermined")]
156+
pub const NOT_DETERMINED: Self = Self(0);
157+
#[doc(alias = "EPDeveloperToolStatusRestricted")]
158+
pub const RESTRICTED: Self = Self(1);
159+
#[doc(alias = "EPDeveloperToolStatusDenied")]
160+
pub const DENIED: Self = Self(2);
161+
#[doc(alias = "EPDeveloperToolStatusAuthorized")]
162+
pub const AUTHORIZED: Self = Self(3);
163+
}
164+
165+
#[cfg(test)]
166+
mod tests {
167+
use super::*;
168+
169+
#[test]
170+
fn does_not_crash() {
171+
let Some(handle) = ExecutionPolicyHandle::open().unwrap() else {
172+
return;
173+
};
174+
175+
let developer_tool = EPDeveloperTool::new(&handle).unwrap();
176+
177+
let _ = developer_tool.authorization_status();
178+
179+
// Test that requesting access doesn't crash either. This might be
180+
// slightly annoying for macOS Cargo developers if they _really_ don't
181+
// want their terminal to show up in their Developer Tools settings,
182+
// but in that case we should probably reconsider this feature.
183+
let _ = developer_tool.request_access().unwrap();
184+
}
185+
}

0 commit comments

Comments
 (0)