use std::collections::BTreeSet;
use std::io;
use std::path::{Path, PathBuf};
use super::system_roots::{
broad_system_root, cached_tree_entry_count, hosts_an_executable, system_read_roots,
};
use super::{process_sandbox_preset_acl_roots, run_icacls, sandbox_trace};
use crate::orchestration::CapabilityPolicy;
use crate::stdlib::sandbox::{
policy_allows_workspace_write, process_sandbox_policy_read_roots,
process_sandbox_policy_write_roots, process_sandbox_readonly_roots, process_sandbox_roots,
};
pub(super) struct WorkspaceAclGrants {
label: String,
sid: String,
paths: Vec<PathBuf>,
}
const ALL_APPLICATION_PACKAGES_SID: &str = "S-1-15-2-1";
#[derive(Clone, Copy, PartialEq, Eq)]
enum Grantee {
ThisContainer,
EveryAppContainer,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum MustExist {
Yes,
No,
}
#[derive(Clone, Copy)]
enum GrantIs {
LoadBearing,
BestEffort,
}
impl WorkspaceAclGrants {
pub(super) fn grant(label: &str, sid: &str, policy: &CapabilityPolicy) -> io::Result<Self> {
let workspace_permission = if policy_allows_workspace_write(policy) {
"(OI)(CI)M"
} else {
"(OI)(CI)RX"
};
let mut paths = Vec::new();
let writable = process_sandbox_roots(policy).into_iter().map(|root| {
(
root,
workspace_permission,
MustExist::Yes,
GrantIs::LoadBearing,
Grantee::ThisContainer,
)
});
let read_only = process_sandbox_readonly_roots(policy)
.into_iter()
.map(|root| {
(
root,
"(OI)(CI)RX",
MustExist::Yes,
GrantIs::BestEffort,
Grantee::ThisContainer,
)
});
let process_read = process_sandbox_policy_read_roots(policy)
.into_iter()
.map(|root| {
(
root,
"(OI)(CI)RX",
MustExist::Yes,
GrantIs::BestEffort,
Grantee::ThisContainer,
)
});
let preset_roots = process_sandbox_preset_acl_roots(policy)
.into_iter()
.map(|root| {
(
root,
"(OI)(CI)RX",
MustExist::No,
GrantIs::BestEffort,
Grantee::ThisContainer,
)
});
let write_roots: Vec<PathBuf> = process_sandbox_roots(policy)
.into_iter()
.chain(process_sandbox_policy_write_roots(policy))
.collect();
let unaffordable = unaffordable_read_roots(policy, &write_roots);
let system_read = system_read_roots().into_iter().map(|root| {
(
root,
"(OI)(CI)RX",
MustExist::No,
GrantIs::BestEffort,
Grantee::EveryAppContainer,
)
});
let process_write = if policy_allows_workspace_write(policy) {
process_sandbox_policy_write_roots(policy)
.into_iter()
.map(|root| {
(
root,
workspace_permission,
MustExist::Yes,
GrantIs::LoadBearing,
Grantee::ThisContainer,
)
})
.collect::<Vec<_>>()
} else {
Vec::new()
};
for (root, permission, must_exist, grant_is, grantee) in writable
.chain(read_only)
.chain(process_read)
.chain(preset_roots)
.chain(system_read)
.chain(process_write)
{
if !root.exists() {
if must_exist == MustExist::No {
continue;
}
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("sandbox workspace root '{}' does not exist", root.display()),
));
}
if unaffordable.contains(&root) {
continue;
}
let grantee_sid = match grantee {
Grantee::ThisContainer => sid,
Grantee::EveryAppContainer => ALL_APPLICATION_PACKAGES_SID,
};
if grantee == Grantee::EveryAppContainer && recheck_reads_open(&root) {
sandbox_trace(
label,
format!(
"icacls grant skipped path={} reason=granted-concurrently-by-another-process",
root.display()
),
);
continue;
}
sandbox_trace(
label,
format!(
"icacls grant begin path={} grantee={}",
root.display(),
match grantee {
Grantee::ThisContainer => "this-container",
Grantee::EveryAppContainer => "every-app-container",
}
),
);
let granted = run_icacls(
&root,
[
"/grant",
&format!("*{grantee_sid}:{permission}"),
"/T",
"/C",
],
);
match (granted, grant_is) {
(Ok(()), _) => {
sandbox_trace(label, "icacls grant ok");
match grantee {
Grantee::ThisContainer => paths.push(root),
Grantee::EveryAppContainer => {
remember_reads_open(&root);
report_durable_host_grant(&root);
}
}
}
(Err(error), GrantIs::LoadBearing) => return Err(error),
(Err(error), GrantIs::BestEffort) => sandbox_trace(
label,
format!(
"icacls grant failed, continuing read-closed for this root: path={} error={error}",
root.display()
),
),
}
}
Ok(Self {
label: label.to_string(),
sid: sid.to_string(),
paths,
})
}
}
impl Drop for WorkspaceAclGrants {
fn drop(&mut self) {
for path in &self.paths {
sandbox_trace(
&self.label,
format!("icacls remove begin path={}", path.display()),
);
match run_icacls(path, ["/remove:g", &format!("*{}", self.sid), "/T", "/C"]) {
Ok(()) => sandbox_trace(&self.label, "icacls remove ok"),
Err(error) => sandbox_trace(&self.label, format!("icacls remove failed: {error}")),
}
}
}
}
const READ_GRANT_ROOT_ENTRY_CEILING: usize = 6144;
const READ_GRANT_TOTAL_ENTRY_CEILING: usize = 65536;
fn unaffordable_read_roots(
policy: &CapabilityPolicy,
write_roots: &[PathBuf],
) -> BTreeSet<PathBuf> {
let mut skip = BTreeSet::new();
let mut covered_by: Vec<PathBuf> = write_roots.to_vec();
let candidates = process_sandbox_readonly_roots(policy)
.into_iter()
.chain(process_sandbox_policy_read_roots(policy))
.chain(process_sandbox_preset_acl_roots(policy))
.map(|root| (root, false))
.chain(system_read_roots().into_iter().map(|root| (root, true)));
let mut priced: Vec<(usize, PathBuf)> = Vec::new();
for (root, from_path) in candidates {
if skip.contains(&root) || priced.iter().any(|(_, seen)| *seen == root) || !root.exists() {
continue;
}
if from_path {
if broad_system_root(&root) {
read_root_decision(&root, "action=skipped reason=broad-system-prefix");
skip.insert(root);
continue;
}
if !hosts_an_executable(&root) {
read_root_decision(&root, "action=skipped reason=no-executable-in-directory");
skip.insert(root);
continue;
}
}
if app_container_can_already_read(&root) {
read_root_decision(
&root,
"probe=already-open action=skipped reason=admits-all-application-packages",
);
skip.insert(root);
continue;
}
let Some(entries) = cached_tree_entry_count(&root, READ_GRANT_ROOT_ENTRY_CEILING) else {
read_root_decision(
&root,
&format!(
"probe=closed action=skipped reason=tree-exceeds-root-ceiling-{READ_GRANT_ROOT_ENTRY_CEILING}"
),
);
skip.insert(root);
continue;
};
priced.push((entries, root));
}
priced.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1)));
let mut remaining = READ_GRANT_TOTAL_ENTRY_CEILING;
for (entries, root) in priced {
if covered_by.iter().any(|already| root.starts_with(already)) {
read_root_decision(
&root,
"action=skipped reason=already-granted-by-another-root",
);
skip.insert(root);
continue;
}
if entries > remaining {
read_root_decision(
&root,
&format!(
"probe=closed action=skipped reason=entries-{entries}-exceed-remaining-spawn-ceiling-{remaining}"
),
);
skip.insert(root);
continue;
}
remaining -= entries;
covered_by.push(root.clone());
read_root_decision(
&root,
&format!(
"probe=closed action=will-grant entries={entries} remaining_budget={remaining}"
),
);
}
skip
}
fn read_root_decision(root: &Path, outcome: &str) {
sandbox_trace(
"read-roots",
format!("decision path={} {outcome}", root.display()),
);
}
fn recheck_reads_open(path: &Path) -> bool {
let Ok(dacl) = read_icacls(path) else {
return false;
};
let dacl = dacl.to_ascii_uppercase();
let readable = dacl.contains("ALL APPLICATION PACKAGES") || dacl.contains("S-1-15-2-1:");
if readable {
remember_reads_open(path);
}
readable
}
fn report_durable_host_grant(root: &Path) {
if !first_report_of(root) {
return;
}
eprintln!(
"harn: opened '{}' for reading by sandboxed programs. This is a lasting \
change to this machine: the entry stays after harn exits, applies to \
every sandboxed program rather than this run alone, and re-enables \
permission inheritance on that directory. Undo it with: icacls \
\"{}\" /remove:g *{} /T /C",
root.display(),
root.display(),
ALL_APPLICATION_PACKAGES_SID
);
}
fn first_report_of(root: &Path) -> bool {
static REPORTED: std::sync::OnceLock<std::sync::Mutex<BTreeSet<PathBuf>>> =
std::sync::OnceLock::new();
let reported = REPORTED.get_or_init(|| std::sync::Mutex::new(BTreeSet::new()));
match reported.lock() {
Ok(mut seen) => seen.insert(root.to_path_buf()),
Err(_) => true,
}
}
fn reads_open_cache() -> &'static std::sync::Mutex<std::collections::HashMap<PathBuf, bool>> {
static CACHE: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<PathBuf, bool>>> =
std::sync::OnceLock::new();
CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
}
fn remember_reads_open(path: &Path) {
if let Ok(mut map) = reads_open_cache().lock() {
map.insert(path.to_path_buf(), true);
}
}
fn app_container_can_already_read(path: &Path) -> bool {
let cache = reads_open_cache();
if let Ok(map) = cache.lock() {
if let Some(known) = map.get(path) {
return *known;
}
}
let Ok(dacl) = read_icacls(path) else {
sandbox_trace(
"system-read-roots",
format!("DACL unreadable, treated as open path={}", path.display()),
);
return true;
};
let dacl = dacl.to_ascii_uppercase();
let readable = dacl.contains("ALL APPLICATION PACKAGES") || dacl.contains("S-1-15-2-1:");
if let Ok(mut map) = cache.lock() {
map.insert(path.to_path_buf(), readable);
}
readable
}
fn read_icacls(path: &Path) -> io::Result<String> {
let output = std::process::Command::new("icacls").arg(path).output()?;
if !output.status.success() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("icacls read failed for '{}'", path.display()),
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}