use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use serde::{Deserialize, Serialize};
use crate::orchestration::CapabilityPolicy;
use crate::security::environment_policy::environment_names_equal_for_platform;
pub const RUSTC_WRAPPER_ENV_KEYS: [&str; 4] = [
"RUSTC_WRAPPER",
"CARGO_BUILD_RUSTC_WRAPPER",
"RUSTC_WORKSPACE_WRAPPER",
"CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER",
];
const PROBE_CRATE: &str = "harn_rustc_wrapper_probe";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RustcWrapperDisposition {
NotConfigured,
Kept,
Disabled,
Unmeasured,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RustcWrapperDecision {
pub disposition: RustcWrapperDisposition,
pub wrapper: Option<String>,
pub reason: String,
pub cwd: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RustcWrapperApplication {
pub program: String,
pub sandbox_profile: crate::orchestration::SandboxProfile,
pub decision: RustcWrapperDecision,
}
impl RustcWrapperApplication {
pub fn decode(payload: serde_json::Value) -> Result<Self, &'static str> {
let application: Self = serde_json::from_value(payload)
.map_err(|_| "wrapper application fields are missing, redacted, or invalid")?;
let identity = [
application.program.as_str(),
application.decision.cwd.as_str(),
application.decision.reason.as_str(),
];
if identity
.iter()
.any(|value| value.is_empty() || value.contains(crate::redact::REDACTED_PLACEHOLDER))
|| application
.decision
.wrapper
.as_ref()
.is_some_and(|wrapper| {
wrapper.is_empty() || wrapper.contains(crate::redact::REDACTED_PLACEHOLDER)
})
|| (application.decision.disposition == RustcWrapperDisposition::Kept
&& application.decision.wrapper.is_none())
{
return Err("wrapper application identity or reason is unavailable after redaction");
}
Ok(application)
}
}
pub(super) fn record_application(
program: &str,
policy: &CapabilityPolicy,
decision: RustcWrapperDecision,
) -> RustcWrapperDecision {
if crate::run_events::sink_active() {
let application = RustcWrapperApplication {
program: program.to_string(),
sandbox_profile: policy.sandbox_profile,
decision: decision.clone(),
};
crate::run_events::emit(crate::run_events::RunEvent::CompilerWrapperApplied {
payload: serde_json::to_value(application)
.expect("wrapper application has only serializable fields"),
});
}
decision
}
impl RustcWrapperDecision {
pub fn disables(&self) -> bool {
self.disposition != RustcWrapperDisposition::Kept
}
pub fn drops_configured_wrapper(&self) -> bool {
matches!(
self.disposition,
RustcWrapperDisposition::Disabled | RustcWrapperDisposition::Unmeasured
)
}
}
type DecisionKey = (String, String, [u8; 32]);
#[path = "rustc_wrapper_cache.rs"]
mod cache_inputs;
struct Entry {
decision: RustcWrapperDecision,
inputs: Option<cache_inputs::Inputs>,
}
fn confinement_key(policy: &CapabilityPolicy) -> String {
serde_json::to_string(&(
policy.sandbox_profile,
&policy.workspace_roots,
&policy.read_only_roots,
&policy.process_sandbox,
policy.children_may_write(),
super::super::policy_allows_network(policy),
policy
.process_network_proxy
.map(|proxy| (proxy.http_port, proxy.socks_port)),
))
.expect("confinement policy is serializable")
}
fn decisions() -> &'static Mutex<BTreeMap<DecisionKey, Entry>> {
static DECISIONS: OnceLock<Mutex<BTreeMap<DecisionKey, Entry>>> = OnceLock::new();
DECISIONS.get_or_init(|| Mutex::new(BTreeMap::new()))
}
thread_local! {
static PROBING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub(crate) fn probing() -> bool {
PROBING.with(std::cell::Cell::get)
}
pub fn rustc_wrapper_decision(
policy: &CapabilityPolicy,
cwd: &Path,
caller_env: &[(String, String)],
) -> RustcWrapperDecision {
let child_env =
crate::stdlib::process::session_closed_env_for_command("cargo", caller_env.iter().cloned());
let env: BTreeMap<String, String> = match child_env {
Ok(Some(env)) => env.into_iter().collect(),
_ => {
let mut env = std::env::vars().collect();
for (key, value) in caller_env {
crate::security::session_environment::insert_env_value(
&mut env,
key,
value.clone(),
);
}
env
}
};
rustc_wrapper_decision_for_environment(policy, cwd, &env)
}
pub(super) fn rustc_wrapper_decision_for_environment(
policy: &CapabilityPolicy,
cwd: &Path,
env: &BTreeMap<String, String>,
) -> RustcWrapperDecision {
let key = (
confinement_key(policy),
cwd.display().to_string(),
*blake3::hash(&serde_json::to_vec(env).expect("environment is serializable")).as_bytes(),
);
let mut cache = decisions()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let inputs = cache_inputs::capture(cwd, env);
if let Some(entry) = cache.get(&key) {
if inputs.is_some() && entry.inputs == inputs {
return entry.decision.clone();
}
}
let child_env: Vec<_> = env
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
let decision = measure(policy, cwd, &child_env);
record(&decision);
cache.insert(
key,
Entry {
decision: decision.clone(),
inputs,
},
);
decision
}
pub fn rustc_wrapper_decisions() -> Vec<RustcWrapperDecision> {
decisions()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.values()
.map(|entry| entry.decision.clone())
.collect()
}
fn record(decision: &RustcWrapperDecision) {
let mut metadata = BTreeMap::new();
metadata.insert(
"disposition".to_string(),
serde_json::to_value(decision.disposition).unwrap_or_default(),
);
metadata.insert("wrapper".to_string(), serde_json::json!(decision.wrapper));
metadata.insert("reason".to_string(), serde_json::json!(decision.reason));
metadata.insert("cwd".to_string(), serde_json::json!(decision.cwd));
let message = match decision.disposition {
RustcWrapperDisposition::Disabled => {
"a Cargo rustc wrapper was switched off in the sandbox"
}
RustcWrapperDisposition::Kept => "a Cargo rustc wrapper was kept in the sandbox",
RustcWrapperDisposition::Unmeasured => {
"a Cargo rustc wrapper could not be measured and was switched off in the sandbox"
}
RustcWrapperDisposition::NotConfigured => "no Cargo rustc wrapper applies",
};
if decision.drops_configured_wrapper() {
crate::events::log_warn_meta("process_sandbox_rustc_wrapper", message, metadata);
} else if decision.disposition == RustcWrapperDisposition::NotConfigured {
crate::events::log_debug_meta("process_sandbox_rustc_wrapper", message, metadata);
} else {
crate::events::log_info_meta("process_sandbox_rustc_wrapper", message, metadata);
}
}
fn decision(
disposition: RustcWrapperDisposition,
wrapper: Option<String>,
reason: impl Into<String>,
cwd: &Path,
) -> RustcWrapperDecision {
RustcWrapperDecision {
disposition,
wrapper,
reason: reason.into(),
cwd: cwd.display().to_string(),
}
}
fn measure(
policy: &CapabilityPolicy,
cwd: &Path,
caller: &[(String, String)],
) -> RustcWrapperDecision {
if crate::testbench::process_tape::active_tape().is_some() {
return decision(
RustcWrapperDisposition::Unmeasured,
None,
"a process tape is active, and the probe would be a spawn the tape does not hold",
cwd,
);
}
let configured = configured_wrapper(cwd, caller);
if configured.is_none() {
return decision(
RustcWrapperDisposition::NotConfigured,
None,
"no wrapper setting in the child's environment and no Cargo configuration that \
could name one",
cwd,
);
}
let Some(scratch_parent) = crate::stdlib::sandbox::workspace_local_tmpdir(policy) else {
return decision(
RustcWrapperDisposition::Unmeasured,
None,
"no workspace-local scratch directory to build the probe crate in",
cwd,
);
};
let scratch = match Scratch::create(&scratch_parent) {
Ok(scratch) => scratch,
Err(error) => {
return decision(
RustcWrapperDisposition::Unmeasured,
None,
format!("could not create the probe crate: {error}"),
cwd,
)
}
};
if let Err(error) = write_probe_crate(scratch.path()) {
return decision(
RustcWrapperDisposition::Unmeasured,
None,
format!("could not write the probe crate: {error}"),
cwd,
);
}
if let Some(known) = configured.as_deref().and_then(known_wrapper) {
known.prepare(caller);
}
let nonce = probe_nonce();
let with_wrapper = match build(scratch.path(), cwd, caller.to_vec(), Some(&nonce)) {
Ok(outcome) => outcome,
Err(reason) => return decision(RustcWrapperDisposition::NotConfigured, None, reason, cwd),
};
let wrapper = with_wrapper.wrapper.clone();
if let Some(session) = &with_wrapper.session {
let survivors = session.members();
if !survivors.is_empty() {
let stopped = session.stop(&survivors);
return decision(
RustcWrapperDisposition::Disabled,
wrapper,
format!(
"the wrapper left {} long-lived process(es) running in the probe build's \
session, which would keep this sandbox and serve later builds with it; \
{stopped} were stopped",
survivors.len()
),
cwd,
);
}
let escaped = escaped_processes(&nonce, session);
if escaped > 0 {
return decision(
RustcWrapperDisposition::Unmeasured,
wrapper,
format!(
"{escaped} process(es) the probe build started left its session, so they \
cannot be proven the probe's to stop and were left running"
),
cwd,
);
}
}
if with_wrapper.success {
return match wrapper {
None => decision(
RustcWrapperDisposition::NotConfigured,
None,
"Cargo resolved no wrapper",
cwd,
),
Some(wrapper) => decision(
RustcWrapperDisposition::Kept,
Some(wrapper),
"a crate compiled through the wrapper under this profile",
cwd,
),
};
}
let blanked = wrapper_free_environment(caller, cfg!(windows));
let without_error = match build(scratch.path(), cwd, blanked, None) {
Ok(without) if without.success => {
return decision(
RustcWrapperDisposition::Disabled,
wrapper.or_else(|| with_wrapper.failed_wrapper.clone()),
format!(
"the wrapper cannot run under this profile: {}",
with_wrapper.error_line
),
cwd,
)
}
Ok(without) => without.error_line,
Err(reason) => reason,
};
decision(
RustcWrapperDisposition::Unmeasured,
wrapper.or(with_wrapper.failed_wrapper),
format!(
"the probe crate failed to build with and without the wrapper: with it: {}; \
without it: {without_error}",
with_wrapper.error_line
),
cwd,
)
}
struct Scratch(PathBuf);
impl Scratch {
fn create(parent: &Path) -> std::io::Result<Self> {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let path = parent.join(format!("rustc-wrapper-probe-{}-{n}", std::process::id()));
std::fs::create_dir_all(&path)?;
Ok(Self(path))
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn wrapper_free_environment(caller: &[(String, String)], windows: bool) -> Vec<(String, String)> {
caller
.iter()
.filter(|(key, _)| {
!RUSTC_WRAPPER_ENV_KEYS
.iter()
.any(|wrapper| environment_names_equal_for_platform(key, wrapper, windows))
})
.cloned()
.chain(
RUSTC_WRAPPER_ENV_KEYS
.iter()
.map(|key| (key.to_string(), String::new())),
)
.collect()
}
fn configured_wrapper(cwd: &Path, caller: &[(String, String)]) -> Option<String> {
configured_wrapper_for_platform(cwd, caller, cfg!(windows))
}
fn configured_wrapper_for_platform(
cwd: &Path,
caller: &[(String, String)],
windows: bool,
) -> Option<String> {
let child_env: BTreeMap<String, String> = caller.iter().cloned().collect();
let value = |key: &str| {
child_env
.iter()
.find(|(name, _)| environment_names_equal_for_platform(name, key, windows))
.map(|(_, value)| value)
};
if let Some(value) = RUSTC_WRAPPER_ENV_KEYS
.iter()
.filter_map(|key| value(key))
.find(|value| !value.is_empty())
{
return Some(value.clone());
}
let cargo_home = value("CARGO_HOME")
.map(PathBuf::from)
.or_else(|| value("HOME").map(|home| Path::new(home).join(".cargo")));
let mut dirs: Vec<PathBuf> = cwd.ancestors().map(|dir| dir.join(".cargo")).collect();
dirs.extend(cargo_home.map(|directory| {
if directory.is_absolute() {
directory
} else {
cwd.join(directory)
}
}));
dirs.iter()
.flat_map(|dir| [dir.join("config"), dir.join("config.toml")])
.filter_map(|file| std::fs::read_to_string(file).ok())
.filter(|text| text.contains("wrapper") || text.contains("include"))
.map(|text| {
text.lines()
.filter(|line| line.trim_start().starts_with("rustc-wrapper"))
.find_map(|line| line.split_once('=').map(|(_, value)| value))
.map(|value| value.trim().trim_matches('"').to_string())
.unwrap_or_default()
})
.next()
}
pub struct KnownWrapper {
pub name_contains: &'static str,
pub prepare: &'static [&'static str],
}
pub const KNOWN_WRAPPERS: &[KnownWrapper] = &[KnownWrapper {
name_contains: "sccache",
prepare: &["sccache", "--start-server"],
}];
fn known_wrapper(configured: &str) -> Option<&'static KnownWrapper> {
let name = Path::new(configured).file_name()?.to_string_lossy();
KNOWN_WRAPPERS
.iter()
.find(|known| name.contains(known.name_contains))
}
impl KnownWrapper {
fn prepare(&self, env: &[(String, String)]) {
let Some((program, args)) = self.prepare.split_first() else {
return;
};
let Ok(mut command) = crate::process_sandbox::session_std_command(program) else {
return;
};
let _ = command
.env_clear()
.envs(env.iter().map(|(key, value)| (key, value)))
.args(args)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
}
}
fn write_probe_crate(root: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(root.join("src"))?;
std::fs::write(
root.join("Cargo.toml"),
format!(
"[package]\nname = \"{PROBE_CRATE}\"\nversion = \"0.0.0\"\nedition = \"2021\"\n\
publish = false\n\n[workspace]\n"
),
)?;
std::fs::write(root.join("src").join("main.rs"), "fn main() {}\n")
}
struct BuildOutcome {
session: Option<ProbeSession>,
success: bool,
wrapper: Option<String>,
failed_wrapper: Option<String>,
error_line: String,
}
fn build(
scratch: &Path,
cwd: &Path,
mut env: Vec<(String, String)>,
nonce: Option<&str>,
) -> Result<BuildOutcome, String> {
let manifest = scratch.join("Cargo.toml");
let target = scratch.join("target");
env.retain(|(key, _)| {
if cfg!(windows) {
!key.eq_ignore_ascii_case("CARGO_BUILD_BUILD_DIR")
} else {
key != "CARGO_BUILD_BUILD_DIR"
}
});
env.push((
"CARGO_BUILD_BUILD_DIR".to_string(),
target.display().to_string(),
));
let args = vec![
"build".to_string(),
"-v".to_string(),
"--color".to_string(),
"never".to_string(),
"--offline".to_string(),
"--manifest-path".to_string(),
manifest.display().to_string(),
"--target-dir".to_string(),
target.display().to_string(),
];
let sandbox = crate::stdlib::sandbox::active_sandbox_policy()
.ok_or_else(|| "no sandbox policy is active".to_string())?;
let (policy, profile) = sandbox;
if let Some(nonce) = nonce {
env.push((PROBE_NONCE_ENV.to_string(), nonce.to_string()));
}
let config = crate::stdlib::sandbox::ProcessCommandConfig {
cwd: Some(PathBuf::from(cwd)),
closed_env: true,
env,
..crate::stdlib::sandbox::ProcessCommandConfig::default()
};
PROBING.with(|flag| flag.set(true));
let output = crate::stdlib::sandbox::sandboxed_process_config("cargo", &args, &config, &policy)
.and_then(|config| {
use crate::stdlib::sandbox::SandboxBackend;
#[cfg(unix)]
{
crate::stdlib::sandbox::ActiveBackend::run_to_output_in_session(
"cargo", &args, &config, &policy, profile,
)
.map(|(output, session)| (output, Some(ProbeSession::started(session))))
}
#[cfg(not(unix))]
{
crate::stdlib::sandbox::ActiveBackend::run_to_output(
"cargo", &args, &config, &policy, profile,
)
.map(|output| (output, None))
}
});
PROBING.with(|flag| flag.set(false));
let (output, session) =
output.map_err(|error| format!("cargo is not reachable from the child: {error:?}"))?;
let stderr = String::from_utf8_lossy(&output.stderr);
Ok(BuildOutcome {
session,
success: output.status.success(),
wrapper: running_wrapper(&stderr),
failed_wrapper: failed_wrapper(&stderr),
error_line: stderr
.lines()
.find(|line| line.trim_start().starts_with("error"))
.unwrap_or_else(|| stderr.lines().last().unwrap_or_default())
.trim()
.to_string(),
})
}
fn running_wrapper(stderr: &str) -> Option<String> {
let line = stderr
.lines()
.find(|line| line.contains("Running `") && line.contains(PROBE_CRATE))?;
let command = line.split_once("Running `")?.1;
let before_crate = command.split(" --crate-name").next()?;
let words: Vec<&str> = before_crate.split_whitespace().collect();
let compiler = words.iter().rposition(|word| {
Path::new(word.trim_matches('`'))
.file_stem()
.is_some_and(|stem| stem == "rustc")
})?;
(compiler > 0).then(|| words[..compiler].join(" "))
}
fn failed_wrapper(stderr: &str) -> Option<String> {
let command = stderr.lines().find_map(|line| {
[
"could not execute process `",
"process didn't exit successfully: `",
]
.iter()
.find_map(|marker| line.split_once(marker))
.and_then(|(_, rest)| rest.split('`').next())
})?;
let words: Vec<&str> = command.split_whitespace().collect();
let compiler = words.iter().rposition(|word| {
Path::new(word)
.file_stem()
.is_some_and(|stem| stem == "rustc")
})?;
(compiler > 0).then(|| words[..compiler].join(" "))
}
const PROBE_NONCE_ENV: &str = "RUSTC_WRAPPER_PROBE_NONCE";
fn probe_nonce() -> String {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let sequence = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!("{}-{sequence}", std::process::id())
}
fn started_sessions() -> &'static Mutex<BTreeSet<u32>> {
static STARTED: OnceLock<Mutex<BTreeSet<u32>>> = OnceLock::new();
STARTED.get_or_init(|| Mutex::new(BTreeSet::new()))
}
struct ProbeSession(u32);
impl ProbeSession {
#[cfg(unix)]
fn started(id: u32) -> Self {
started_sessions()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.insert(id);
Self(id)
}
fn registered(&self) -> bool {
started_sessions()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.contains(&self.0)
}
fn members(&self) -> Vec<u32> {
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};
let mut system = System::new();
system.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
ProcessRefreshKind::nothing(),
);
system
.processes()
.keys()
.map(|pid| pid.as_u32())
.filter(|pid| self.holds(*pid))
.collect()
}
#[cfg(unix)]
fn holds(&self, pid: u32) -> bool {
pid != self.0
&& pid != std::process::id()
&& unsafe { libc::getsid(pid as libc::pid_t) } == self.0 as libc::pid_t
}
#[cfg(not(unix))]
fn holds(&self, _pid: u32) -> bool {
false
}
fn stop(&self, pids: &[u32]) -> usize {
if !self.registered() {
return 0;
}
pids.iter()
.filter(|pid| self.holds(**pid) && signal(**pid))
.count()
}
}
#[cfg(unix)]
fn signal(pid: u32) -> bool {
unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) == 0 }
}
#[cfg(not(unix))]
fn signal(_pid: u32) -> bool {
false
}
#[cfg(target_os = "linux")]
fn escaped_processes(nonce: &str, session: &ProbeSession) -> usize {
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind};
let entry = format!("{PROBE_NONCE_ENV}={nonce}");
let mut system = System::new();
system.refresh_processes_specifics(
ProcessesToUpdate::All,
true,
ProcessRefreshKind::nothing().with_environ(UpdateKind::Always),
);
system
.processes()
.iter()
.filter(|(pid, process)| {
pid.as_u32() != std::process::id()
&& !session.holds(pid.as_u32())
&& process
.environ()
.iter()
.any(|variable| variable.to_str() == Some(entry.as_str()))
})
.count()
}
#[cfg(not(target_os = "linux"))]
fn escaped_processes(_nonce: &str, _session: &ProbeSession) -> usize {
0
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
fn session_with_one_member() -> u32 {
use std::os::unix::process::CommandExt;
let mut command = std::process::Command::new("/bin/sleep");
command.arg("60");
unsafe {
command.pre_exec(|| {
if libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
match libc::fork() {
-1 => Err(std::io::Error::last_os_error()),
0 => Ok(()),
_ => libc::_exit(0),
}
});
}
let mut leader = command.spawn().expect("start session leader");
let session = leader.id();
leader.wait().expect("leader exits");
session
}
#[cfg(unix)]
fn emptied(session: &ProbeSession) -> bool {
(0..5_000).any(|_| {
std::thread::yield_now();
session.members().is_empty()
})
}
#[cfg(unix)]
#[test]
fn only_a_member_of_the_probes_session_is_stopped() {
let session = ProbeSession::started(session_with_one_member());
let mut bystander = std::process::Command::new("/bin/sleep");
bystander.arg("60");
crate::op_interrupt::configure_kill_group(&mut bystander);
let mut bystander = bystander.spawn().expect("start bystander");
let members = session.members();
assert_eq!(members.len(), 1, "{members:?}");
assert!(!members.contains(&bystander.id()));
assert_eq!(session.stop(&members), 1);
assert!(emptied(&session), "the member outlived its signal");
assert!(
bystander.try_wait().expect("poll bystander").is_none(),
"a process outside the session was stopped"
);
bystander.kill().expect("stop bystander");
let _ = bystander.wait();
}
#[cfg(unix)]
#[test]
fn a_session_no_probe_started_is_never_signalled() {
let leader = session_with_one_member();
let unregistered = ProbeSession(leader);
let members = unregistered.members();
assert_eq!(members.len(), 1, "{members:?}");
assert_eq!(unregistered.stop(&members), 0);
assert_eq!(unregistered.members(), members, "a member was signalled");
let session = ProbeSession::started(leader);
assert_eq!(session.stop(&members), 1);
assert!(emptied(&session), "the member outlived its signal");
}
#[test]
fn the_running_line_names_the_wrapper_chain_before_the_compiler() {
let stderr = " Compiling harn_rustc_wrapper_probe v0.0.0\n Running `/home/u/bin/log-wrapper /home/u/.rustup/toolchains/x/bin/rustc --crate-name harn_rustc_wrapper_probe --edition=2021 src/main.rs`\n";
assert_eq!(
running_wrapper(stderr).as_deref(),
Some("/home/u/bin/log-wrapper")
);
let bare = " Running `/home/u/.rustup/toolchains/x/bin/rustc --crate-name harn_rustc_wrapper_probe src/main.rs`\n";
assert_eq!(running_wrapper(bare), None);
}
#[test]
fn a_failed_execution_names_the_wrapper_cargo_tried() {
let stderr =
"error: could not execute process `/missing/wrapper /x/rustc -vV` (never executed)\n";
assert_eq!(failed_wrapper(stderr).as_deref(), Some("/missing/wrapper"));
}
#[test]
fn confinement_key_ignores_agent_policy_but_preserves_process_authority() {
let policy = CapabilityPolicy::default();
let mut tool_policy = policy.clone();
tool_policy.tools = vec!["other_tool".into()];
tool_policy.recursion_limit = Some(0);
assert_eq!(confinement_key(&policy), confinement_key(&tool_policy));
tool_policy
.process_sandbox
.read_roots
.push("/other/root".into());
assert_ne!(confinement_key(&policy), confinement_key(&tool_policy));
}
#[test]
fn wrapper_lookup_and_control_build_follow_platform_environment_names() {
let workspace = tempfile::tempdir().unwrap();
let env = vec![("Rustc_Wrapper".into(), "mixed-case-wrapper".into())];
assert_eq!(
configured_wrapper_for_platform(workspace.path(), &env, true).as_deref(),
Some("mixed-case-wrapper")
);
assert_eq!(
configured_wrapper_for_platform(workspace.path(), &env, false),
None
);
let windows = wrapper_free_environment(&env, true);
assert!(!windows.iter().any(|(key, _)| key == "Rustc_Wrapper"));
assert!(RUSTC_WRAPPER_ENV_KEYS.iter().all(|key| windows
.iter()
.any(|(name, value)| name == *key && value.is_empty())));
let posix = wrapper_free_environment(&env, false);
assert!(
posix.contains(&env[0]),
"POSIX mixed-case names are unrelated variables"
);
}
}