use std::path::{Path, PathBuf};
use murmur_artifact::{security_warning_link, ContainmentClass, W_SEC_012, W_SEC_029};
use crate::errors::{RuntimeError, UnreachableEntrypoint};
use crate::types::CapabilityPolicy;
const SHEBANG_PROBE_BYTES: u64 = 512;
const KNOWN_TOOLCHAIN_DRIVERS: &[(&str, &[&str])] = &[
("cc", &["cc1", "cc1plus", "as", "ld", "collect2"]),
("gcc", &["cc1", "cc1plus", "as", "ld", "collect2"]),
("g++", &["cc1plus", "as", "ld", "collect2"]),
("c++", &["cc1plus", "as", "ld", "collect2"]),
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolchainHelperWarning {
pub driver: String,
pub helper: String,
pub resolved_path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnprobedToolchainDriver {
pub driver: String,
pub driver_path: PathBuf,
pub unchecked_helpers: Vec<String>,
pub error_kind: Option<std::io::ErrorKind>,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ToolchainHelperReport {
pub uncovered: Vec<ToolchainHelperWarning>,
pub unprobed: Vec<UnprobedToolchainDriver>,
}
pub(crate) fn shebang_interpreter_name(path: &Path) -> Option<String> {
let line = shebang_line(path, SHEBANG_PROBE_BYTES)?;
let mut tokens = line.split_whitespace();
let target = base_name(tokens.next()?)?;
if target != "env" {
return Some(target);
}
for token in tokens {
if token.starts_with('-') || token.contains('=') {
continue;
}
return base_name(token);
}
Some(target)
}
pub(crate) fn shebang_line(path: &Path, limit: u64) -> Option<String> {
use std::io::Read;
let mut head = Vec::new();
std::fs::File::open(path)
.ok()?
.take(limit)
.read_to_end(&mut head)
.ok()?;
shebang_line_of(&head)
}
pub(crate) fn shebang_line_of(head: &[u8]) -> Option<String> {
let rest = head.strip_prefix(b"#!")?;
let line_end = rest
.iter()
.position(|&byte| byte == b'\n' || byte == b'\r')
.unwrap_or(rest.len());
Some(String::from_utf8_lossy(&rest[..line_end]).into_owned())
}
fn base_name(raw: &str) -> Option<String> {
Path::new(raw)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
}
pub fn check_interpreted_entrypoints_reachable(
policy: &CapabilityPolicy,
declared_floor: ContainmentClass,
) -> Result<(), RuntimeError> {
check_interpreted_entrypoints_reachable_in(policy, declared_floor, &host_path_dirs())
}
fn check_interpreted_entrypoints_reachable_in(
policy: &CapabilityPolicy,
declared_floor: ContainmentClass,
path_dirs: &[PathBuf],
) -> Result<(), RuntimeError> {
if declared_floor != ContainmentClass::Sealed {
return Ok(());
}
let fixed_dirs = sealed_runtime_paths();
let declared_dirs = declared_grant_dirs(policy);
let mut entries: Vec<UnreachableEntrypoint> = Vec::new();
for entry in &policy.shell_allow {
if entry.is_empty() {
continue;
}
let resolved = PathBuf::from(crate::sandbox::resolve_invoked_binary_path_in(
entry, path_dirs,
));
if !resolved.is_absolute() {
continue;
}
if under_any(&resolved, &fixed_dirs) || under_any(&resolved, &declared_dirs) {
continue;
}
let Some(interpreter) = shebang_interpreter_name(&resolved) else {
continue;
};
if declared_grant_binaries(policy)
.any(|binary| binary == entry.as_str() || binary == interpreter.as_str())
{
continue;
}
entries.push(UnreachableEntrypoint {
binary: entry.clone(),
resolved_path: resolved,
interpreter,
});
}
if entries.is_empty() {
return Ok(());
}
Err(RuntimeError::ShellBinaryPackageUnreachable { entries })
}
pub fn warn_on_unreachable_toolchain_helpers(
policy: &CapabilityPolicy,
declared_floor: ContainmentClass,
) -> ToolchainHelperReport {
let report = unreachable_toolchain_helpers_in(policy, declared_floor, &host_path_dirs());
for warning in &report.uncovered {
let link = security_warning_link(W_SEC_012);
crate::runtime_err!(
"[capsule-runtime] warning[{W_SEC_012}]: capabilities.shell.allow grants the compiler \
driver '{}', but its helper '{}' at {} has no grant carrying the Landlock Execute \
right under the 'sealed' composed root — the fixed sealed runtime tree ({}) is bound \
read-only and listable but deliberately not executable, so the driver will start and \
then fail partway through a real compile; declare \
capabilities.shell.interpreter_runtime or staged_runtime for '{}' naming that \
helper's directory ({}) to grant it ({link})",
warning.driver,
warning.helper,
warning.resolved_path.display(),
crate::sealed::SEALED_RUNTIME_PATHS.join(", "),
warning.driver,
warning
.resolved_path
.parent()
.unwrap_or_else(|| Path::new("/"))
.display(),
);
}
for unprobed in &report.unprobed {
let link = security_warning_link(W_SEC_029);
crate::runtime_err!(
"[capsule-runtime] warning[{W_SEC_029}]: capabilities.shell.allow grants the compiler \
driver '{}', but running {} -print-prog-name=<helper> failed ({}), so W-SEC-012 was \
not evaluated for its helpers [{}] and they may have no Execute grant under the \
'sealed' composed root; check that {} can be executed by this user and re-run \
`mur doctor` ({link})",
unprobed.driver,
unprobed.driver_path.display(),
unprobed.reason,
unprobed.unchecked_helpers.join(", "),
unprobed.driver_path.display(),
);
}
report
}
fn unreachable_toolchain_helpers_in(
policy: &CapabilityPolicy,
declared_floor: ContainmentClass,
path_dirs: &[PathBuf],
) -> ToolchainHelperReport {
let mut report = ToolchainHelperReport::default();
if declared_floor != ContainmentClass::Sealed {
return report;
}
let declared_dirs = declared_grant_dirs(policy);
let allowlisted_paths: Vec<PathBuf> = policy
.shell_allow
.iter()
.filter(|entry| !entry.is_empty())
.map(|entry| {
PathBuf::from(crate::sandbox::resolve_invoked_binary_path_in(
entry, path_dirs,
))
})
.collect();
for entry in &policy.shell_allow {
let Some((_, helpers)) = KNOWN_TOOLCHAIN_DRIVERS
.iter()
.find(|(driver, _)| base_name(entry).as_deref() == Some(*driver))
else {
continue;
};
let driver_path = PathBuf::from(crate::sandbox::resolve_invoked_binary_path_in(
entry, path_dirs,
));
if !driver_path.is_absolute() {
continue;
}
for (index, helper) in helpers.iter().enumerate() {
let resolved_path = match probe_helper_path(&driver_path, helper, path_dirs) {
HelperProbe::Located(path) => path,
HelperProbe::NothingUsable => continue,
HelperProbe::NotAsked { error_kind, reason } => {
let unprobed = UnprobedToolchainDriver {
driver: entry.clone(),
driver_path: driver_path.clone(),
unchecked_helpers: helpers[index..]
.iter()
.map(|name| (*name).to_string())
.collect(),
error_kind,
reason,
};
if !report.unprobed.contains(&unprobed) {
report.unprobed.push(unprobed);
}
break;
}
};
if under_any(&resolved_path, &declared_dirs)
|| policy.shell_allow.iter().any(|allowed| allowed == helper)
|| allowlisted_paths.contains(&resolved_path)
{
continue;
}
let warning = ToolchainHelperWarning {
driver: entry.clone(),
helper: (*helper).to_string(),
resolved_path,
};
if !report.uncovered.contains(&warning) {
report.uncovered.push(warning);
}
}
}
report
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum HelperProbe {
Located(PathBuf),
NothingUsable,
NotAsked {
error_kind: Option<std::io::ErrorKind>,
reason: String,
},
}
fn probe_helper_path(driver: &Path, helper: &str, path_dirs: &[PathBuf]) -> HelperProbe {
let output = match std::process::Command::new(driver)
.arg(format!("-print-prog-name={helper}"))
.output()
{
Ok(output) => output,
Err(error) => {
return HelperProbe::NotAsked {
error_kind: Some(error.kind()),
reason: error.to_string(),
}
}
};
if let Some(reason) = terminating_signal(&output.status) {
return HelperProbe::NotAsked {
error_kind: None,
reason,
};
}
if !output.status.success() {
return HelperProbe::NothingUsable;
}
let printed = String::from_utf8_lossy(&output.stdout).trim().to_string();
if printed.is_empty() {
return HelperProbe::NothingUsable;
}
let candidate = if printed == helper {
PathBuf::from(crate::sandbox::resolve_invoked_binary_path_in(
helper, path_dirs,
))
} else {
PathBuf::from(&printed)
};
if !candidate.is_absolute() {
return HelperProbe::NothingUsable;
}
match std::fs::canonicalize(candidate) {
Ok(path) => HelperProbe::Located(path),
Err(_) => HelperProbe::NothingUsable,
}
}
#[cfg(unix)]
fn terminating_signal(status: &std::process::ExitStatus) -> Option<String> {
use std::os::unix::process::ExitStatusExt;
status
.signal()
.map(|signal| format!("terminated by signal {signal}"))
}
#[cfg(not(unix))]
fn terminating_signal(_status: &std::process::ExitStatus) -> Option<String> {
None
}
fn host_path_dirs() -> Vec<PathBuf> {
std::env::var_os("PATH")
.map(|path| std::env::split_paths(&path).collect())
.unwrap_or_default()
}
fn sealed_runtime_paths() -> Vec<PathBuf> {
crate::sealed::SEALED_RUNTIME_PATHS
.iter()
.map(PathBuf::from)
.collect()
}
fn declared_grant_dirs(policy: &CapabilityPolicy) -> Vec<PathBuf> {
policy
.shell_staged_runtime
.iter()
.map(|grant| PathBuf::from(&grant.source_path))
.chain(
policy
.shell_interpreter_runtime
.iter()
.flat_map(|grant| grant.dirs.iter().map(|dir| PathBuf::from(&dir.path))),
)
.filter(|path| path.is_absolute())
.collect()
}
fn declared_grant_binaries(policy: &CapabilityPolicy) -> impl Iterator<Item = &str> {
policy
.shell_staged_runtime
.iter()
.map(|grant| grant.binary.as_str())
.chain(
policy
.shell_interpreter_runtime
.iter()
.map(|grant| grant.binary.as_str()),
)
}
pub(crate) fn under_any(path: &Path, roots: &[PathBuf]) -> bool {
roots.iter().any(|root| path.starts_with(root))
}
#[cfg(test)]
mod tests {
use super::*;
use murmur_artifact::{InterpreterRuntimeDir, InterpreterRuntimeGrant, StagedRuntimeGrant};
const ALL_CLASSES: &[ContainmentClass] = &[
ContainmentClass::Advisory,
ContainmentClass::Scoped,
ContainmentClass::Sealed,
];
fn write_binary(dir: &Path, name: &str, contents: &[u8]) -> PathBuf {
let path = dir.join(name);
std::fs::write(&path, contents).expect("write fixture");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755))
.expect("chmod fixture");
}
path
}
fn script(dir: &Path, name: &str, shebang: &str) -> PathBuf {
write_binary(dir, name, format!("{shebang}\nprint('hi')\n").as_bytes())
}
fn elf(dir: &Path, name: &str) -> PathBuf {
let mut bytes = b"\x7fELF\x02\x01\x01\x00".to_vec();
bytes.resize(128, 0);
write_binary(dir, name, &bytes)
}
fn policy_allowing(names: &[&str]) -> CapabilityPolicy {
CapabilityPolicy {
shell_allow: names.iter().map(|name| (*name).to_string()).collect(),
..CapabilityPolicy::default()
}
}
fn interpreter_grant(binary: &str, dirs: &[&str]) -> InterpreterRuntimeGrant {
InterpreterRuntimeGrant {
binary: binary.to_string(),
dirs: dirs
.iter()
.map(|path| InterpreterRuntimeDir {
path: (*path).to_string(),
list_dir: true,
})
.collect(),
}
}
fn staged_grant(binary: &str, source_path: &str) -> StagedRuntimeGrant {
StagedRuntimeGrant {
binary: binary.to_string(),
source_path: source_path.to_string(),
pin: "test-pin-1".to_string(),
}
}
#[test]
fn shebang_returns_the_interpreter_basename_for_a_direct_target() {
let dir = tempfile::tempdir().expect("tempdir");
let path = script(dir.path(), "direct", "#!/usr/bin/python3.12");
assert_eq!(
shebang_interpreter_name(&path),
Some("python3.12".to_string())
);
}
#[test]
fn shebang_resolves_one_level_of_env_indirection() {
let dir = tempfile::tempdir().expect("tempdir");
for (name, line, expected) in [
("plain-env", "#!/usr/bin/env python3", "python3"),
("split-string", "#!/usr/bin/env -S python3 -u", "python3"),
("assignment", "#!/usr/bin/env FOO=bar node", "node"),
("bare-env", "#!/usr/bin/env", "env"),
] {
let path = script(dir.path(), name, line);
assert_eq!(
shebang_interpreter_name(&path),
Some(expected.to_string()),
"shebang line: {line}"
);
}
}
#[test]
fn shebang_stops_at_a_carriage_return() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_binary(
dir.path(),
"crlf",
b"#!/usr/bin/env python3\r\nprint(1)\r\n",
);
assert_eq!(shebang_interpreter_name(&path), Some("python3".to_string()));
}
#[test]
fn shebang_is_none_for_an_elf_image_and_for_unreadable_paths() {
let dir = tempfile::tempdir().expect("tempdir");
assert_eq!(shebang_interpreter_name(&elf(dir.path(), "binary")), None);
assert_eq!(
shebang_interpreter_name(&write_binary(dir.path(), "plain", b"just text\n")),
None
);
assert_eq!(
shebang_interpreter_name(&dir.path().join("does-not-exist")),
None
);
assert_eq!(shebang_interpreter_name(dir.path()), None);
}
#[test]
fn shebang_probe_reads_a_bounded_prefix_only() {
let dir = tempfile::tempdir().expect("tempdir");
let mut bytes = vec![b'#'; SHEBANG_PROBE_BYTES as usize * 4];
bytes.extend_from_slice(b"\n#!/usr/bin/env python3\n");
assert_eq!(
shebang_interpreter_name(&write_binary(dir.path(), "huge", &bytes)),
None
);
}
#[test]
fn an_uncovered_script_entrypoint_refuses_naming_binary_path_and_interpreter() {
let dir = tempfile::tempdir().expect("tempdir");
script(dir.path(), "pip", "#!/usr/bin/env python3");
let dirs = vec![dir.path().to_path_buf()];
let error = check_interpreted_entrypoints_reachable_in(
&policy_allowing(&["pip"]),
ContainmentClass::Sealed,
&dirs,
)
.expect_err("an uncovered interpreted entrypoint must refuse");
let rendered = error.to_string();
assert!(rendered.contains("pip"), "{rendered}");
assert!(
rendered.contains(&dir.path().join("pip").display().to_string()),
"{rendered}"
);
assert!(rendered.contains("python3"), "{rendered}");
assert!(rendered.contains("interpreter_runtime"), "{rendered}");
assert!(rendered.contains("strace"), "{rendered}");
}
#[test]
fn a_grant_naming_the_script_or_its_interpreter_is_sufficient() {
let dir = tempfile::tempdir().expect("tempdir");
script(dir.path(), "pip", "#!/usr/bin/env python3");
let dirs = vec![dir.path().to_path_buf()];
for policy in [
CapabilityPolicy {
shell_interpreter_runtime: vec![interpreter_grant("python3", &["/opt/unrelated"])],
..policy_allowing(&["pip"])
},
CapabilityPolicy {
shell_staged_runtime: vec![staged_grant("pip", "/opt/unrelated")],
..policy_allowing(&["pip"])
},
] {
assert!(
check_interpreted_entrypoints_reachable_in(
&policy,
ContainmentClass::Sealed,
&dirs
)
.is_ok(),
"a declared covering grant must be sufficient"
);
}
}
#[test]
fn a_script_inside_a_declared_grant_directory_is_covered_by_residence() {
let dir = tempfile::tempdir().expect("tempdir");
script(dir.path(), "pip", "#!/usr/bin/env python3");
let dirs = vec![dir.path().to_path_buf()];
let policy = CapabilityPolicy {
shell_staged_runtime: vec![staged_grant(
"some-other-binary",
dir.path().to_str().expect("utf-8 temp path"),
)],
..policy_allowing(&["pip"])
};
assert!(check_interpreted_entrypoints_reachable_in(
&policy,
ContainmentClass::Sealed,
&dirs
)
.is_ok());
}
#[test]
fn a_script_under_a_fixed_sealed_runtime_path_needs_no_grant() {
for entry in crate::sealed::SEALED_RUNTIME_PATHS {
let policy = policy_allowing(&[&format!("{entry}/bin/some-console-script")]);
assert!(
check_interpreted_entrypoints_reachable_in(&policy, ContainmentClass::Sealed, &[])
.is_ok(),
"a path under {entry} must never be collected"
);
}
}
#[test]
fn prefix_matching_is_component_wise_not_textual() {
assert!(under_any(
Path::new("/usr/bin/pip"),
&[PathBuf::from("/usr")]
));
assert!(!under_any(
Path::new("/usrlocal/bin/pip"),
&[PathBuf::from("/usr")]
));
}
#[test]
fn elf_entrypoints_are_never_collected() {
let dir = tempfile::tempdir().expect("tempdir");
elf(dir.path(), "bash");
let dirs = vec![dir.path().to_path_buf()];
assert!(check_interpreted_entrypoints_reachable_in(
&policy_allowing(&["bash"]),
ContainmentClass::Sealed,
&dirs
)
.is_ok());
}
#[test]
fn an_unresolvable_entry_is_skipped_rather_than_refused() {
assert!(check_interpreted_entrypoints_reachable_in(
&policy_allowing(&["definitely-not-installed-anywhere"]),
ContainmentClass::Sealed,
&[]
)
.is_ok());
}
#[test]
fn the_refusal_names_every_unreachable_entrypoint() {
let dir = tempfile::tempdir().expect("tempdir");
script(dir.path(), "pip", "#!/usr/bin/env python3");
script(dir.path(), "rake", "#!/usr/bin/env ruby");
let dirs = vec![dir.path().to_path_buf()];
let error = check_interpreted_entrypoints_reachable_in(
&policy_allowing(&["pip", "rake"]),
ContainmentClass::Sealed,
&dirs,
)
.expect_err("both entrypoints are uncovered");
let rendered = error.to_string();
for expected in ["pip", "python3", "rake", "ruby"] {
assert!(
rendered.contains(expected),
"missing {expected}: {rendered}"
);
}
}
#[test]
fn both_checks_are_inert_below_a_declared_sealed_floor() {
let dir = tempfile::tempdir().expect("tempdir");
script(dir.path(), "pip", "#!/usr/bin/env python3");
let dirs = vec![dir.path().to_path_buf()];
let policy = policy_allowing(&["pip", "cc"]);
for class in ALL_CLASSES {
let refusal = check_interpreted_entrypoints_reachable_in(&policy, *class, &dirs);
let report = unreachable_toolchain_helpers_in(&policy, *class, &dirs);
if *class == ContainmentClass::Sealed {
assert!(refusal.is_err(), "sealed must still be checked");
} else {
assert!(refusal.is_ok(), "{class} must not be refused");
assert!(report.uncovered.is_empty(), "{class} must not warn");
assert!(
report.unprobed.is_empty(),
"{class} must not probe anything: {report:?}"
);
}
}
}
#[cfg(unix)]
fn fake_driver(dir: &Path, name: &str, libexec: &Path) -> PathBuf {
let script = format!(
"#!/bin/sh\n\
case \"$1\" in\n\
-print-prog-name=cc1) echo {libexec}/cc1 ;;\n\
-print-prog-name=cc1plus) echo {libexec}/cc1plus ;;\n\
-print-prog-name=collect2) echo {libexec}/collect2 ;;\n\
-print-prog-name=as) echo as ;;\n\
-print-prog-name=ld) echo ld ;;\n\
*) exit 1 ;;\n\
esac\n",
libexec = libexec.display()
);
let path = write_binary(dir, name, script.as_bytes());
settle_exec_fixture(&path);
path
}
#[cfg(unix)]
const SETTLE_EXEC_ATTEMPTS: u32 = 200;
#[cfg(unix)]
const SETTLE_EXEC_INTERVAL: std::time::Duration = std::time::Duration::from_millis(10);
#[cfg(unix)]
fn settle_exec_fixture(path: &Path) {
for _ in 0..SETTLE_EXEC_ATTEMPTS {
match std::process::Command::new(path).output() {
Err(error) if error.kind() == std::io::ErrorKind::ExecutableFileBusy => {
std::thread::sleep(SETTLE_EXEC_INTERVAL);
}
_ => return,
}
}
panic!(
"{} was still text file busy after {SETTLE_EXEC_ATTEMPTS} exec attempts",
path.display()
);
}
#[cfg(unix)]
#[test]
fn an_uncovered_toolchain_helper_warns_naming_driver_helper_and_path() {
let dir = tempfile::tempdir().expect("tempdir");
let libexec = dir.path().join("libexec");
std::fs::create_dir_all(&libexec).expect("libexec");
for helper in ["cc1", "cc1plus", "collect2"] {
write_binary(&libexec, helper, b"#!/bin/sh\nexit 0\n");
}
fake_driver(dir.path(), "cc", &libexec);
let dirs = vec![dir.path().to_path_buf()];
let report = unreachable_toolchain_helpers_in(
&policy_allowing(&["cc"]),
ContainmentClass::Sealed,
&dirs,
);
assert!(
report.unprobed.is_empty(),
"the driver must be probed: {report:?}"
);
let cc1 = report
.uncovered
.iter()
.find(|warning| warning.helper == "cc1")
.unwrap_or_else(|| panic!("cc1 must be reported as uncovered: {report:?}"));
assert_eq!(cc1.driver, "cc");
assert_eq!(
cc1.resolved_path,
std::fs::canonicalize(libexec.join("cc1")).expect("canonical cc1")
);
}
#[cfg(unix)]
#[test]
fn a_declared_grant_directory_covers_the_helpers_inside_it() {
let dir = tempfile::tempdir().expect("tempdir");
let libexec = dir.path().join("libexec");
std::fs::create_dir_all(&libexec).expect("libexec");
for helper in ["cc1", "cc1plus", "collect2"] {
write_binary(&libexec, helper, b"#!/bin/sh\nexit 0\n");
}
fake_driver(dir.path(), "cc", &libexec);
let dirs = vec![dir.path().to_path_buf()];
let policy = CapabilityPolicy {
shell_interpreter_runtime: vec![interpreter_grant(
"cc",
&[libexec.to_str().expect("utf-8 temp path")],
)],
..policy_allowing(&["cc"])
};
let report = unreachable_toolchain_helpers_in(&policy, ContainmentClass::Sealed, &dirs);
assert!(
report.unprobed.is_empty(),
"the driver must be probed: {report:?}"
);
assert!(
!report
.uncovered
.iter()
.any(|warning| warning.helper == "cc1"),
"a declared grant directory must cover its helpers: {report:?}"
);
}
#[cfg(unix)]
#[test]
fn a_path_deferred_helper_answer_is_resolved_not_discarded() {
let dir = tempfile::tempdir().expect("tempdir");
let libexec = dir.path().join("libexec");
std::fs::create_dir_all(&libexec).expect("libexec");
write_binary(dir.path(), "as", b"#!/bin/sh\nexit 0\n");
fake_driver(dir.path(), "cc", &libexec);
let dirs = vec![dir.path().to_path_buf()];
let report = unreachable_toolchain_helpers_in(
&policy_allowing(&["cc"]),
ContainmentClass::Sealed,
&dirs,
);
assert!(
report.unprobed.is_empty(),
"the driver must be probed: {report:?}"
);
let assembler = report
.uncovered
.iter()
.find(|warning| warning.helper == "as")
.unwrap_or_else(|| panic!("a PATH-deferred helper must still be checked: {report:?}"));
assert_eq!(
assembler.resolved_path,
std::fs::canonicalize(dir.path().join("as")).expect("canonical as")
);
}
#[test]
fn residence_under_the_fixed_sealed_tree_is_not_coverage() {
let policy = CapabilityPolicy {
shell_interpreter_runtime: Vec::new(),
shell_staged_runtime: Vec::new(),
..policy_allowing(&["cc"])
};
assert!(under_any(Path::new("/usr/bin/as"), &sealed_runtime_paths()));
assert!(
!under_any(Path::new("/usr/bin/as"), &declared_grant_dirs(&policy)),
"the fixed tree must never be folded into the declared-grant set"
);
}
#[cfg(unix)]
#[test]
fn a_helper_that_is_itself_allowlisted_is_covered() {
let dir = tempfile::tempdir().expect("tempdir");
let libexec = dir.path().join("libexec");
std::fs::create_dir_all(&libexec).expect("libexec");
write_binary(&libexec, "cc1", b"#!/bin/sh\nexit 0\n");
fake_driver(dir.path(), "cc", &libexec);
let dirs = vec![dir.path().to_path_buf()];
let report = unreachable_toolchain_helpers_in(
&policy_allowing(&["cc", "cc1"]),
ContainmentClass::Sealed,
&dirs,
);
assert!(
report.unprobed.is_empty(),
"the driver must be probed: {report:?}"
);
assert!(
!report
.uncovered
.iter()
.any(|warning| warning.helper == "cc1"),
"an allowlisted helper carries its own Execute grant: {report:?}"
);
}
#[cfg(unix)]
#[test]
fn a_non_driver_entry_is_never_probed() {
let dir = tempfile::tempdir().expect("tempdir");
fake_driver(dir.path(), "not-a-compiler", &dir.path().join("libexec"));
let dirs = vec![dir.path().to_path_buf()];
assert_eq!(
unreachable_toolchain_helpers_in(
&policy_allowing(&["not-a-compiler"]),
ContainmentClass::Sealed,
&dirs
),
ToolchainHelperReport::default()
);
}
fn cc_row() -> Vec<String> {
["cc1", "cc1plus", "as", "ld", "collect2"]
.iter()
.map(|helper| (*helper).to_string())
.collect()
}
#[cfg(unix)]
#[test]
fn a_driver_that_cannot_be_executed_is_reported_as_unprobed() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().expect("tempdir");
let driver = write_binary(dir.path(), "cc", b"#!/bin/sh\nexit 0\n");
std::fs::set_permissions(&driver, std::fs::Permissions::from_mode(0o644))
.expect("chmod 0644");
let entry = driver.to_str().expect("utf-8 temp path");
let report = unreachable_toolchain_helpers_in(
&policy_allowing(&[entry]),
ContainmentClass::Sealed,
&[],
);
assert!(report.uncovered.is_empty(), "{report:?}");
assert_eq!(report.unprobed.len(), 1, "{report:?}");
let unprobed = &report.unprobed[0];
assert_eq!(unprobed.driver, entry);
assert_eq!(
unprobed.driver_path,
std::fs::canonicalize(&driver).expect("canonical driver")
);
assert_eq!(
unprobed.error_kind,
Some(std::io::ErrorKind::PermissionDenied),
"{report:?}"
);
assert_eq!(unprobed.unchecked_helpers, cc_row());
}
#[cfg(target_os = "linux")]
#[test]
fn a_driver_busy_being_written_is_reported_as_unprobed() {
let dir = tempfile::tempdir().expect("tempdir");
let libexec = dir.path().join("libexec");
std::fs::create_dir_all(&libexec).expect("libexec");
for helper in ["cc1", "cc1plus", "collect2"] {
write_binary(&libexec, helper, b"#!/bin/sh\nexit 0\n");
}
let driver = write_binary(
dir.path(),
"cc",
format!(
"#!/bin/sh\n\
case \"$1\" in\n\
-print-prog-name=cc1) echo {libexec}/cc1 ;;\n\
*) exit 1 ;;\n\
esac\n",
libexec = libexec.display()
)
.as_bytes(),
);
let dirs = vec![dir.path().to_path_buf()];
let writer = std::fs::OpenOptions::new()
.append(true)
.open(&driver)
.expect("open driver for append");
let report = unreachable_toolchain_helpers_in(
&policy_allowing(&["cc"]),
ContainmentClass::Sealed,
&dirs,
);
drop(writer);
assert!(report.uncovered.is_empty(), "{report:?}");
assert_eq!(report.unprobed.len(), 1, "{report:?}");
assert_eq!(
report.unprobed[0].error_kind,
Some(std::io::ErrorKind::ExecutableFileBusy),
"{report:?}"
);
assert_eq!(report.unprobed[0].unchecked_helpers, cc_row());
}
#[cfg(unix)]
#[test]
fn a_driver_killed_before_answering_is_reported_as_unprobed() {
let dir = tempfile::tempdir().expect("tempdir");
let driver = write_binary(dir.path(), "cc", b"#!/bin/sh\nkill -9 $$\n");
settle_exec_fixture(&driver);
let dirs = vec![dir.path().to_path_buf()];
let report = unreachable_toolchain_helpers_in(
&policy_allowing(&["cc"]),
ContainmentClass::Sealed,
&dirs,
);
assert!(report.uncovered.is_empty(), "{report:?}");
assert_eq!(report.unprobed.len(), 1, "{report:?}");
assert_eq!(report.unprobed[0].error_kind, None, "{report:?}");
assert!(report.unprobed[0].reason.contains("signal 9"), "{report:?}");
assert_eq!(report.unprobed[0].unchecked_helpers, cc_row());
}
#[cfg(unix)]
#[test]
fn a_driver_that_does_not_understand_the_flag_contributes_nothing() {
let dir = tempfile::tempdir().expect("tempdir");
let driver = write_binary(dir.path(), "cc", b"#!/bin/sh\nexit 1\n");
settle_exec_fixture(&driver);
let dirs = vec![dir.path().to_path_buf()];
let report = unreachable_toolchain_helpers_in(
&policy_allowing(&["cc"]),
ContainmentClass::Sealed,
&dirs,
);
assert_eq!(report, ToolchainHelperReport::default());
}
#[cfg(target_os = "linux")]
#[test]
fn the_exec_settle_retries_only_while_busy() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_binary(dir.path(), "busy", b"#!/bin/sh\nexit 0\n");
let writer = std::fs::OpenOptions::new()
.append(true)
.open(&path)
.expect("open fixture for append");
let while_busy = std::panic::catch_unwind(|| settle_exec_fixture(&path));
assert!(
while_busy.is_err(),
"a held write handle must exhaust the bound"
);
drop(writer);
settle_exec_fixture(&path);
}
#[test]
fn the_known_driver_registry_is_the_documented_four() {
let drivers: Vec<&str> = KNOWN_TOOLCHAIN_DRIVERS
.iter()
.map(|(driver, _)| *driver)
.collect();
assert_eq!(drivers, vec!["cc", "gcc", "g++", "c++"]);
for (driver, helpers) in KNOWN_TOOLCHAIN_DRIVERS {
assert!(
helpers.contains(&"as") && helpers.contains(&"ld"),
"{driver} must be probed for the assembler and linker it execs"
);
}
}
}