pub mod fs; use std::borrow::Cow; use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] pub enum CapabilitiesError { #[error("Unknown capability: {0}")] UnknownCapability(String), #[error("Unknown system information name: {0}")] UnknownSys(String), #[error("Environment variable names must be nonempty and contain neither NUL nor '='")] InvalidEnvName, #[error("Invalid network grant: {0}")] InvalidNetGrant(String), #[error("Invalid execution URL: {0}")] InvalidExecUrl(String), #[error("Invalid path grant: {0}")] InvalidPathGrant(String), #[error("Invalid file URL grant: {0}")] InvalidFileUrlGrant(String), #[error("File URL grants cannot contain queries or fragments")] FileUrlQueryFragment, #[error("Invalid empty or NUL-containing path grant")] EmptyPathGrant, #[error("Encoded path separators are not supported")] EncodedSeparator, #[error("Remote paths must be UTF-8")] RemoteNotUtf8, #[error("Expected a hierarchical module URL")] NotHierarchical, #[error("Invalid remote path segment")] InvalidRemoteSegment, #[error("Network checks require a host and separate port")] NetworkRequiresHostPort, #[error("PermissionDenied: env {0}")] EnvDenied(String), #[error("PermissionDenied: sys {0}")] SysDenied(String), #[error("PermissionDenied: net {0}:{1}")] NetDenied(String, u16), #[error("PermissionDenied: net {0}")] NetUrlDenied(String), #[error("PermissionDenied: read {}", .0.display())] ReadDenied(PathBuf), #[error("PermissionDenied: write {}", .0.display())] WriteDenied(PathBuf), #[error("PermissionDenied: run {}", .0.display())] RunDenied(PathBuf), #[error("PermissionDenied: ffi {}", .0.display())] FfiDenied(PathBuf), #[error("PermissionDenied: ffi")] FfiDeniedAny, #[error("PermissionDenied: read tree {}", .0.display())] ReadTreeDenied(PathBuf), #[error("PermissionDenied: write tree {}", .0.display())] WriteTreeDenied(PathBuf), #[error("PermissionDenied: js-exec {}", .0.display())] JsExecDenied(PathBuf), #[error("PermissionDenied: js-exec {0}")] RemoteAcquireDenied(String), #[error("PermissionDenied: js-exec {0}")] RemoteExecDenied(String), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Category { Net, Read, Write, JsExec, Env, Sys, Run, Ffi, } // The CLI spells one name js-exec and the JS pledge API spells it jsExec. impl std::str::FromStr for Category { type Err = CapabilitiesError; fn from_str(value: &str) -> Result { Ok(match value { "net" => Self::Net, "read" => Self::Read, "write" => Self::Write, "js-exec" | "jsExec" => Self::JsExec, "env" => Self::Env, "sys" => Self::Sys, "run" => Self::Run, "ffi" => Self::Ffi, _ => return Err(CapabilitiesError::UnknownCapability(value.to_owned())), }) } } pub fn sys_name(value: &str) -> Result<&str, CapabilitiesError> { match value { "hostname" | "osRelease" | "uptime" | "loadavg" => Ok(value), _ => Err(CapabilitiesError::UnknownSys(value.to_owned())), } } pub fn env_name(value: &str) -> Result, CapabilitiesError> { if value.is_empty() || value.contains(['\0', '=']) { return Err(CapabilitiesError::InvalidEnvName); } if cfg!(windows) { Ok(Cow::Owned(value.to_ascii_uppercase())) } else { Ok(Cow::Borrowed(value)) } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct NetGrant { host: url::Host, port: Option, } impl NetGrant { pub fn parse(value: &str) -> Result { let invalid = || CapabilitiesError::InvalidNetGrant(value.to_owned()); if value.is_empty() || value .chars() .any(|c| c.is_whitespace() || ",/@?#\\%*".contains(c)) { return Err(invalid()); } let (host, port) = value .rsplit_once(':') .filter(|_| !value.ends_with(']')) .map_or((value, None), |(host, port)| (host, Some(port))); let port = port .map(|port| { if port.is_empty() || !port.bytes().all(|c| c.is_ascii_digit()) { return Err(invalid()); } port.parse::().map_err(|_| invalid()) }) .transpose()?; let mut host = url::Host::parse(host).map_err(|_| invalid())?; if let url::Host::Domain(name) = &mut host { if name.ends_with('.') { name.pop(); } if name.is_empty() { return Err(invalid()); } } Ok(Self { host, port }) } fn matches(&self, target: &Self) -> bool { self.host == target.host && (self.port.is_none() || self.port == target.port) } } pub fn canonical_remote_url(url: &url::Url) -> Result { let lower = url.path().to_ascii_lowercase(); if lower.contains("%2f") || lower.contains("%5c") { return Err(CapabilitiesError::EncodedSeparator); } let decoded = percent_encoding::percent_decode_str(url.path()) .decode_utf8() .map_err(|_| CapabilitiesError::RemoteNotUtf8)?; let mut canonical = url.clone(); { let mut segments = canonical .path_segments_mut() .map_err(|_| CapabilitiesError::NotHierarchical)?; segments.clear(); for segment in decoded.strip_prefix('/').unwrap_or(&decoded).split('/') { if matches!(segment, "." | "..") || segment.contains('\\') || segment.chars().any(char::is_control) { return Err(CapabilitiesError::InvalidRemoteSegment); } segments.push(segment); } } Ok(canonical) } #[derive(Clone, Debug)] pub struct RemoteGrant { url: url::Url, subtree: bool, } impl RemoteGrant { fn parse(value: &str) -> Result { let url = url::Url::parse(value) .map_err(|_| CapabilitiesError::InvalidExecUrl(value.to_owned()))?; if !matches!(url.scheme(), "http" | "https") || url.host().is_none() || !url.username().is_empty() || url.password().is_some() || url.query().is_some() || url.fragment().is_some() { return Err(CapabilitiesError::InvalidExecUrl(value.to_owned())); } let url = canonical_remote_url(&url)?; let subtree = url.path().ends_with('/'); Ok(Self { url, subtree }) } fn overlaps(&self, base: &url::Url) -> bool { self.matches(base) || (self.url.origin() == base.origin() && self.url.path().starts_with(base.path())) } fn matches(&self, target: &url::Url) -> bool { self.url.origin() == target.origin() && (self.url.path() == target.path() || self.subtree && if self.url.path().ends_with('/') { target.path().starts_with(self.url.path()) } else { target.path().starts_with(&format!("{}/", self.url.path())) }) } } fn remote_execution(value: &str) -> bool { value.split_once(':').is_some_and(|(scheme, _)| { scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https") }) } #[derive(Clone, Debug)] pub struct PathTarget(PathBuf); impl PathTarget { pub fn resolve(path: impl AsRef) -> std::io::Result { path.as_ref().canonicalize().map(Self) } /// Resolve the existing prefix of a possibly missing path. Opening must refuse /// symlinks in every component, including dangling symlinks in the missing tail. pub fn for_creation(path: impl AsRef) -> std::io::Result { let path = if path.as_ref().is_absolute() { path.as_ref().to_owned() } else { std::env::current_dir()?.join(path) }; resolve_prefix(&path).map(|(path, _)| Self(path)) } /// Resolve parents but preserve the final directory entry for no-follow metadata. pub fn for_entry(path: impl AsRef) -> std::io::Result { let path = path.as_ref(); match path.components().next_back() { Some(std::path::Component::Normal(name)) => { let parent = Self::for_creation(path.parent().unwrap_or_else(|| Path::new(".")))?; Ok(Self(parent.into_path().join(name))) } _ => Self::for_creation(path), } } /// Like `for_entry`, but an existing entry takes its stored spelling, as /// canonicalization gives its parents, so case-insensitive filesystems /// cannot respell an executable past a run grant or denial. pub fn for_executable(path: impl AsRef) -> std::io::Result { let Self(path) = Self::for_entry(path)?; stored_spelling(path).map(Self) } pub fn child(&self, name: impl AsRef) -> std::io::Result { let path = Path::new(name.as_ref()); let mut components = path.components(); if !matches!(components.next(), Some(std::path::Component::Normal(_))) || components.next().is_some() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "Expected a single entry name", )); } Ok(Self(self.0.join(path))) } pub fn as_path(&self) -> &Path { &self.0 } pub fn into_path(self) -> PathBuf { self.0 } } /// Lookup failures keep the lexical name: anything that stops this no-follow /// lookup also stops opening or executing the entry by that path. #[cfg(target_os = "macos")] fn stored_spelling(path: PathBuf) -> std::io::Result { use std::os::unix::ffi::{OsStrExt, OsStringExt}; use std::os::unix::fs::MetadataExt; #[repr(C)] struct Attributes { length: u32, name: libc::attrreference_t, data: [u8; 1024], } let text = std::ffi::CString::new(path.as_os_str().as_bytes())?; let mut request = libc::attrlist { bitmapcount: libc::ATTR_BIT_MAP_COUNT, reserved: 0, commonattr: libc::ATTR_CMN_NAME, volattr: 0, dirattr: 0, fileattr: 0, forkattr: 0, }; let mut attributes: Attributes = unsafe { std::mem::zeroed() }; if unsafe { libc::getattrlist( text.as_ptr(), (&raw mut request).cast(), (&raw mut attributes).cast(), std::mem::size_of::(), libc::FSOPT_NOFOLLOW, ) } < 0 { return Ok(path); } // The NUL-terminated name lies at an offset from its reference. let start = (std::mem::offset_of!(Attributes, name) as isize + attributes.name.attr_dataoffset as isize - std::mem::offset_of!(Attributes, data) as isize) as usize; let length = attributes.name.attr_length as usize; let Some(name) = attributes .data .get(start..start.saturating_add(length)) .and_then(|name| name.strip_suffix(&[0])) else { return Err(std::io::Error::other("Malformed file name attribute")); }; let stored = path.with_file_name(std::ffi::OsString::from_vec(name.to_vec())); // A hard-linked file may report another link's name; keep it only if // it names this same entry's file here. let same = |path: &Path| path.symlink_metadata().map(|meta| (meta.dev(), meta.ino())); if stored != path && same(&stored).ok() != same(&path).ok() { return Ok(path); } Ok(stored) } #[cfg(windows)] fn stored_spelling(path: PathBuf) -> std::io::Result { use std::os::windows::ffi::{OsStrExt, OsStringExt}; use std::os::windows::fs::OpenOptionsExt; use std::os::windows::io::AsRawHandle; use windows_sys::Win32::Storage::FileSystem::{ FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_NAME_NORMALIZED, FILE_READ_ATTRIBUTES, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, GetFinalPathNameByHandleW, VOLUME_NAME_NONE, }; // A stream suffix names a different object than the entry. let name = path.file_name().unwrap_or_default(); if name.encode_wide().any(|unit| unit == u16::from(b':')) { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "Invalid file name", )); } // Opening a known name needs no permission to list its directory, so a // lookup that could fail where the launch succeeds must not stand in. let file = match std::fs::OpenOptions::new() .access_mode(FILE_READ_ATTRIBUTES) .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE) .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS) .open(&path) { Ok(file) => file, Err(error) if matches!( error.kind(), std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory ) => { return Ok(path); } Err(error) => return Err(error), }; let mut buffer = vec![0u16; 512]; loop { let length = unsafe { GetFinalPathNameByHandleW( file.as_raw_handle(), buffer.as_mut_ptr(), buffer.len() as u32, FILE_NAME_NORMALIZED | VOLUME_NAME_NONE, ) } as usize; if length == 0 { return Err(std::io::Error::last_os_error()); } if length < buffer.len() { buffer.truncate(length); break; } buffer.resize(length, 0); } // The long name as stored, also undoing 8.3 short names. let stored = PathBuf::from(std::ffi::OsString::from_wide(&buffer)); let name = stored .file_name() .ok_or_else(|| std::io::Error::other("Entry has no stored name"))?; Ok(path.with_file_name(name)) } #[cfg(not(any(target_os = "macos", windows)))] fn stored_spelling(path: PathBuf) -> std::io::Result { Ok(path) } fn resolve_prefix(path: &Path) -> std::io::Result<(PathBuf, bool)> { let mut input = path.to_owned(); let mut redirects = 0; 'resolve: loop { let mut ancestor = input.as_path(); let mut tail = Vec::new(); let mut resolved = loop { match ancestor.canonicalize() { Ok(path) => break path, Err(error) if error.kind() == std::io::ErrorKind::NotFound => { // A dangling link is an existing entry, not a missing lexical tail. match ancestor.symlink_metadata() { Ok(metadata) if metadata.is_symlink() => { if redirects == 40 { return Err(std::io::Error::other( "Too many symbolic links while resolving path", )); } let contents = ancestor.read_link()?; let mut target = ancestor.parent().ok_or(error)?.join(contents); for name in tail.into_iter().rev() { target.push(name); } input = target; redirects += 1; continue 'resolve; } Err(error) if error.kind() != std::io::ErrorKind::NotFound => { return Err(error); } _ => (), } let Some(std::path::Component::Normal(name)) = ancestor.components().next_back() else { return Err(std::io::Error::new( std::io::ErrorKind::NotFound, "Missing parent cannot resolve path components", )); }; tail.push(name); ancestor = ancestor.parent().ok_or(error)?; } Err(error) => return Err(error), } }; let existing = tail.is_empty(); for name in tail.into_iter().rev() { resolved.push(name); } return Ok((resolved, existing)); } } #[derive(Clone, Debug)] pub struct PathGrant { path: PathBuf, directory: bool, } impl PathGrant { pub fn parse(value: &str, cwd: &Path) -> Result { Self::parse_with_directory(value, cwd, true, false) } fn parse_file(value: &str, cwd: &Path) -> Result { let directory = value.ends_with('/') || (cfg!(windows) && value.ends_with('\\')); Self::parse_with_directory(value, cwd, directory, false) } fn parse_run(value: &str, cwd: &Path) -> Result { let directory = value.ends_with('/') || (cfg!(windows) && value.ends_with('\\')); Self::parse_with_directory(value, cwd, directory, true) } fn parse_with_directory( value: &str, cwd: &Path, missing_directory: bool, executable_entry: bool, ) -> Result { if value.is_empty() || value.contains('\0') { return Err(CapabilitiesError::EmptyPathGrant); } let path = if value .get(..5) .is_some_and(|scheme| scheme.eq_ignore_ascii_case("file:")) { let url = url::Url::parse(value) .map_err(|_| CapabilitiesError::InvalidFileUrlGrant(value.to_owned()))?; if url.query().is_some() || url.fragment().is_some() { return Err(CapabilitiesError::FileUrlQueryFragment); } url.to_file_path() .map_err(|_| CapabilitiesError::InvalidFileUrlGrant(value.to_owned()))? } else { cwd.join(value) }; // Multi-call executables must not turn one granted entry into authority for all aliases. if executable_entry && !missing_directory && !path.is_dir() { return Ok(Self { path: PathTarget::for_executable(&path) .map_err(|_| CapabilitiesError::InvalidPathGrant(value.to_owned()))? .into_path(), directory: false, }); } // Resolve existing symlinks, including the existing prefix of a not-yet-created path. let (path, existing) = resolve_prefix(&path) .map_err(|_| CapabilitiesError::InvalidPathGrant(value.to_owned()))?; let directory = if existing { path.is_dir() } else { missing_directory }; Ok(Self { path, directory }) } fn matches(&self, target: &Path) -> bool { if self.directory { target.starts_with(&self.path) } else { target == self.path } } } #[derive(Clone, Debug)] struct Policy { all: bool, allow: Vec, deny: Vec, retain: Vec>, } impl Default for Policy { fn default() -> Self { Self { all: true, allow: Vec::new(), deny: Vec::new(), retain: Vec::new(), } } } impl Policy { fn granted(&self, matches: impl Fn(&G) -> bool) -> bool { self.all || self.allow.iter().any(matches) } fn bounded(&self, matches: impl Fn(&G) -> bool) -> bool { !self.deny.iter().any(&matches) && self.retain.iter().all(|list| list.iter().any(&matches)) } fn permits(&self, matches: impl Fn(&G) -> bool) -> bool { self.granted(&matches) && self.bounded(matches) } fn restrict(&mut self, allow: Option>, deny: Vec) { if let Some(allow) = allow { self.retain.push(allow); } self.deny.extend(deny); } } #[derive(Clone, Default)] pub struct Initial { env: Policy, sys: Policy, net: Policy, read: Policy, write: Policy, js_exec: Policy, run: Policy, ffi: Policy, js_remote: Policy, } impl Initial { pub fn new(zero: bool) -> Self { let mut initial = Self::default(); initial.env.all = !zero; initial.sys.all = !zero; initial.net.all = !zero; initial.read.all = !zero; initial.write.all = !zero; initial.js_exec.all = !zero; initial.run.all = !zero; initial.ffi.all = !zero; initial.js_remote.all = !zero; initial } pub fn grant( &mut self, category: Category, deny: bool, value: &str, cwd: &Path, ) -> Result<(), CapabilitiesError> { if category == Category::JsExec && remote_execution(value) { let mut grant = RemoteGrant::parse(value)?; if deny { grant.subtree = true; self.js_remote.deny.push(grant); } else { self.js_remote.allow.push(grant); } return Ok(()); } match category { Category::Env => { let name = env_name(value)?.into_owned(); if deny { self.env.deny.push(name); } else { self.env.allow.push(name); } } Category::Sys => { let name = sys_name(value)?.to_owned(); if deny { self.sys.deny.push(name); } else { self.sys.allow.push(name); } } Category::Net => { let grant = NetGrant::parse(value)?; if deny { self.net.deny.push(grant); } else { self.net.allow.push(grant); } } Category::Read | Category::Write | Category::JsExec | Category::Run | Category::Ffi => { let mut grant = if matches!(category, Category::Run | Category::Ffi) { PathGrant::parse_run(value, cwd)? } else if category == Category::Write { PathGrant::parse_file(value, cwd)? } else { PathGrant::parse(value, cwd)? }; let policy = match category { Category::Read => &mut self.read, Category::Write => &mut self.write, Category::Run => &mut self.run, Category::Ffi => &mut self.ffi, _ => &mut self.js_exec, }; if deny { grant.directory = true; policy.deny.push(grant); } else { policy.allow.push(grant); } } } Ok(()) } pub fn remote_entry(&mut self, url: &url::Url) -> Result<(), CapabilitiesError> { self.js_remote.allow.push(RemoteGrant { url: canonical_remote_url(url)?, subtree: false, }); Ok(()) } pub fn entry(&mut self, path: &Path) { self.js_exec.allow.push(PathGrant { path: path.to_owned(), directory: false, }); } } // Stored parents are immutable snapshots, never live ceilings. pub struct Capabilities { initial: Initial, restrictions: RwLock, parent: Option>, cwd: PathBuf, } pub type Pledge = Vec<(Category, Option>, Vec)>; enum Restriction { Env(Option>, Vec), Sys(Option>, Vec), Net(Option>, Vec), Read(Option>, Vec), Write(Option>, Vec), JsExec(Option>, Vec), Run(Option>, Vec), Ffi(Option>, Vec), JsRemote(Option>, Vec), } impl Capabilities { pub fn new(initial: Initial, cwd: PathBuf) -> Arc { Arc::new(Self { initial, restrictions: RwLock::new(Initial::default()), parent: None, cwd, }) } pub fn inherit(parent: Arc) -> Arc { // The chain above the parent is already frozen, so cloning this policy // under one lock and sharing the chain is a complete atomic snapshot. let snapshot = Self { initial: parent.initial.clone(), restrictions: RwLock::new(parent.restrictions.read().unwrap().clone()), parent: parent.parent.clone(), cwd: parent.cwd.clone(), }; Arc::new(Self { initial: Initial::default(), restrictions: RwLock::new(Initial::default()), cwd: parent.cwd.clone(), parent: Some(Arc::new(snapshot)), }) } pub fn pledge(&self, request: Pledge) -> Result<(), CapabilitiesError> { let mut prepared = Vec::new(); for (category, allow, deny) in request { let restriction = match category { Category::Sys => Restriction::Sys( allow .map(|values| { values .iter() .map(|s| sys_name(s).map(str::to_owned)) .collect() }) .transpose()?, deny.iter() .map(|s| sys_name(s).map(str::to_owned)) .collect::>()?, ), Category::Env => Restriction::Env( allow .map(|values| { values .iter() .map(|s| env_name(s).map(Cow::into_owned)) .collect() }) .transpose()?, deny.iter() .map(|s| env_name(s).map(Cow::into_owned)) .collect::>()?, ), Category::Net => Restriction::Net( allow .map(|values| { values .iter() .map(|s| NetGrant::parse(s)) .collect::, _>>() }) .transpose()?, deny.iter() .map(|s| NetGrant::parse(s)) .collect::, _>>()?, ), Category::JsExec => { let parse = |values: Vec| { let mut paths = Vec::new(); let mut remote = Vec::new(); for value in values { if remote_execution(&value) { remote.push(RemoteGrant::parse(&value)?); } else { paths.push(PathGrant::parse(&value, &self.cwd)?); } } Ok::<_, CapabilitiesError>((paths, remote)) }; let allow = allow.map(parse).transpose()?; let (mut paths, mut remote) = parse(deny)?; for grant in &mut paths { grant.directory = true; } for grant in &mut remote { grant.subtree = true; } prepared.push(Restriction::JsExec( allow.as_ref().map(|(paths, _)| paths.clone()), paths, )); Restriction::JsRemote(allow.map(|(_, remote)| remote), remote) } Category::Read | Category::Write | Category::Run | Category::Ffi => { let parse = |values: Vec| { values .iter() .map(|s| { if matches!(category, Category::Run | Category::Ffi) { PathGrant::parse_run(s, &self.cwd) } else if category == Category::Write { PathGrant::parse_file(s, &self.cwd) } else { PathGrant::parse(s, &self.cwd) } }) .collect::, _>>() }; let allow = allow.map(parse).transpose()?; let mut deny = parse(deny)?; for grant in &mut deny { grant.directory = true; } match category { Category::Read => Restriction::Read(allow, deny), Category::Write => Restriction::Write(allow, deny), Category::Run => Restriction::Run(allow, deny), Category::Ffi => Restriction::Ffi(allow, deny), _ => unreachable!(), } } }; prepared.push(restriction); } let mut state = self.restrictions.write().unwrap(); for restriction in prepared { match restriction { Restriction::Env(allow, deny) => state.env.restrict(allow, deny), Restriction::Sys(allow, deny) => state.sys.restrict(allow, deny), Restriction::Net(allow, deny) => state.net.restrict(allow, deny), Restriction::Read(allow, deny) => state.read.restrict(allow, deny), Restriction::Write(allow, deny) => state.write.restrict(allow, deny), Restriction::JsExec(allow, deny) => state.js_exec.restrict(allow, deny), Restriction::Run(allow, deny) => state.run.restrict(allow, deny), Restriction::Ffi(allow, deny) => state.ffi.restrict(allow, deny), Restriction::JsRemote(allow, deny) => state.js_remote.restrict(allow, deny), } } Ok(()) } pub fn check_run(&self, target: &PathTarget) -> Result<(), CapabilitiesError> { let matches = |grant: &PathGrant| grant.matches(target.as_path()); let state = self.restrictions.read().unwrap(); if self.initial.run.permits(matches) && state.run.permits(matches) { if let Some(parent) = &self.parent { return parent.check_run(target); } return Ok(()); } Err(CapabilitiesError::RunDenied(target.as_path().to_owned())) } pub fn check_ffi(&self, target: &PathTarget) -> Result<(), CapabilitiesError> { let matches = |grant: &PathGrant| grant.matches(target.as_path()); let state = self.restrictions.read().unwrap(); if self.initial.ffi.permits(matches) && state.ffi.permits(matches) { if let Some(parent) = &self.parent { return parent.check_ffi(target); } return Ok(()); } Err(CapabilitiesError::FfiDenied(target.as_path().to_owned())) } /// Whether any ffi authority remains at all. Path grants count: holding /// authority to load a specific library is enough to use pointer and /// function-pointer operations, which name no path of their own. This /// looks for a witness path that every layer (initial grants, pledges, /// and parents) would permit, so deny rules and disjoint retain lists /// count as withholding authority. pub fn check_ffi_any(&self) -> Result<(), CapabilitiesError> { let mut policies = vec![self.initial.ffi.clone()]; { let state = self.restrictions.read().unwrap(); policies.push(state.ffi.clone()); } if let Some(parent) = &self.parent { parent.collect_ffi_policies(&mut policies); } // Grant spellings cannot contain NUL, so only a root deny covers this. let mut witnesses = vec![PathBuf::from("/\0")]; for policy in &policies { witnesses.extend(policy.allow.iter().map(|grant| grant.path.clone())); witnesses.extend( policy .retain .iter() .flatten() .map(|grant| grant.path.clone()), ); } let permitted = witnesses.iter().any(|witness| { policies .iter() .all(|policy| policy.permits(|grant| grant.matches(witness))) }); if permitted { Ok(()) } else { Err(CapabilitiesError::FfiDeniedAny) } } fn collect_ffi_policies(&self, policies: &mut Vec>) { policies.push(self.initial.ffi.clone()); { let state = self.restrictions.read().unwrap(); policies.push(state.ffi.clone()); } if let Some(parent) = &self.parent { parent.collect_ffi_policies(policies); } } pub fn unrestricted_env(&self) -> bool { let state = self.restrictions.read().unwrap(); self.initial.env.all && self.initial.env.deny.is_empty() && self.initial.env.retain.is_empty() && state.env.deny.is_empty() && state.env.retain.is_empty() && self .parent .as_ref() .is_none_or(|parent| parent.unrestricted_env()) } pub fn check_env(&self, name: &str) -> Result<(), CapabilitiesError> { self.check_env_name(&env_name(name)?) } fn check_env_name(&self, name: &str) -> Result<(), CapabilitiesError> { let state = self.restrictions.read().unwrap(); let matches = |grant: &String| grant == name; if self.initial.env.permits(matches) && state.env.permits(matches) { if let Some(parent) = &self.parent { return parent.check_env_name(name); } return Ok(()); } Err(CapabilitiesError::EnvDenied(name.to_owned())) } pub fn check_sys(&self, name: &str) -> Result<(), CapabilitiesError> { self.check_sys_name(sys_name(name)?) } fn check_sys_name(&self, name: &str) -> Result<(), CapabilitiesError> { let state = self.restrictions.read().unwrap(); let matches = |grant: &String| grant == name; if self.initial.sys.permits(matches) && state.sys.permits(matches) { if let Some(parent) = &self.parent { return parent.check_sys_name(name); } return Ok(()); } Err(CapabilitiesError::SysDenied(name.to_owned())) } pub fn check_net(&self, host: &str, port: u16) -> Result<(), CapabilitiesError> { let mut target = match host.parse::() { Ok(ip) => NetGrant { host: url::Host::Ipv6(ip), port: None, }, Err(_) => NetGrant::parse(host)?, }; if target.port.is_some() { return Err(CapabilitiesError::NetworkRequiresHostPort); } target.port = Some(port); if self.permits_net(&target) { Ok(()) } else { Err(CapabilitiesError::NetDenied(host.to_owned(), port)) } } fn permits_net(&self, target: &NetGrant) -> bool { let state = self.restrictions.read().unwrap(); let matches = |grant: &NetGrant| grant.matches(target); self.initial.net.permits(matches) && state.net.permits(matches) && self .parent .as_ref() .is_none_or(|parent| parent.permits_net(target)) } pub fn check_read(&self, target: &PathTarget) -> Result<(), CapabilitiesError> { let path = target.as_path(); let state = self.restrictions.read().unwrap(); let matches = |grant: &PathGrant| grant.matches(path); if self.initial.read.permits(matches) && state.read.permits(matches) { if let Some(parent) = &self.parent { return parent.check_read(target); } return Ok(()); } Err(CapabilitiesError::ReadDenied(path.to_owned())) } pub fn cwd(&self) -> &Path { &self.cwd } pub fn check_write(&self, target: &PathTarget) -> Result<(), CapabilitiesError> { let path = target.as_path(); let state = self.restrictions.read().unwrap(); let matches = |grant: &PathGrant| grant.matches(path); if self.initial.write.permits(matches) && state.write.permits(matches) { if let Some(parent) = &self.parent { return parent.check_write(target); } return Ok(()); } Err(CapabilitiesError::WriteDenied(path.to_owned())) } pub fn check_read_tree(&self, target: &PathTarget) -> Result<(), CapabilitiesError> { self.check_tree(target, false) } pub fn check_write_tree(&self, target: &PathTarget) -> Result<(), CapabilitiesError> { self.check_tree(target, true) } fn check_tree(&self, target: &PathTarget, write: bool) -> Result<(), CapabilitiesError> { let root = target.as_path(); let permits = |policy: &Policy| { let covers = |grant: &PathGrant| grant.directory && root.starts_with(&grant.path); (policy.all || policy.allow.iter().any(covers)) && policy.retain.iter().all(|list| list.iter().any(covers)) && !policy .deny .iter() .any(|grant| root.starts_with(&grant.path) || grant.path.starts_with(root)) }; let state = self.restrictions.read().unwrap(); let initial = if write { &self.initial.write } else { &self.initial.read }; let restriction = if write { &state.write } else { &state.read }; if permits(initial) && permits(restriction) && self .parent .as_ref() .is_none_or(|parent| parent.check_tree(target, write).is_ok()) { Ok(()) } else { Err(if write { CapabilitiesError::WriteTreeDenied(root.to_owned()) } else { CapabilitiesError::ReadTreeDenied(root.to_owned()) }) } } pub fn unrestricted_run(&self) -> bool { let state = self.restrictions.read().unwrap(); self.initial.run.all && self.initial.run.deny.is_empty() && self.initial.run.retain.is_empty() && state.run.deny.is_empty() && state.run.retain.is_empty() && self .parent .as_ref() .is_none_or(|parent| parent.unrestricted_run()) } pub fn unrestricted_ffi(&self) -> bool { let state = self.restrictions.read().unwrap(); self.initial.ffi.all && self.initial.ffi.deny.is_empty() && self.initial.ffi.retain.is_empty() && state.ffi.deny.is_empty() && state.ffi.retain.is_empty() && self .parent .as_ref() .is_none_or(|parent| parent.unrestricted_ffi()) } pub fn unrestricted_read(&self) -> bool { self.unrestricted_path(false) } pub fn unrestricted_write(&self) -> bool { self.unrestricted_path(true) } fn unrestricted_path(&self, write: bool) -> bool { let state = self.restrictions.read().unwrap(); let initial = if write { &self.initial.write } else { &self.initial.read }; let restriction = if write { &state.write } else { &state.read }; initial.all && initial.deny.is_empty() && initial.retain.is_empty() && restriction.deny.is_empty() && restriction.retain.is_empty() && self .parent .as_ref() .is_none_or(|parent| parent.unrestricted_path(write)) } pub fn unrestricted_js_exec(&self) -> bool { let state = self.restrictions.read().unwrap(); let unrestricted = self.initial.js_exec.deny.is_empty() && self.initial.js_exec.retain.is_empty() && state.js_exec.deny.is_empty() && state.js_exec.retain.is_empty() && (self.initial.js_exec.all || (self.initial.read.all && self.initial.read.deny.is_empty() && state.read.deny.is_empty() && state.read.retain.is_empty())); unrestricted && self .parent .as_ref() .is_none_or(|parent| parent.unrestricted_js_exec()) } pub fn check_remote_acquire(&self, package: &url::Url) -> Result<(), CapabilitiesError> { let canonical = canonical_remote_url(package)?; let package = &canonical; let state = self.restrictions.read().unwrap(); let potential = |policy: &Policy| { (policy.all || policy.allow.iter().any(|grant| grant.overlaps(package))) && !policy.deny.iter().any(|grant| grant.matches(package)) && policy .retain .iter() .all(|list| list.iter().any(|grant| grant.overlaps(package))) }; if potential(&self.initial.js_remote) && potential(&state.js_remote) { if let Some(parent) = &self.parent { return parent.check_remote_acquire(package); } return Ok(()); } Err(CapabilitiesError::RemoteAcquireDenied(package.to_string())) } pub fn check_remote_exec(&self, target: &url::Url) -> Result<(), CapabilitiesError> { let canonical = canonical_remote_url(target)?; let target = &canonical; let state = self.restrictions.read().unwrap(); let matches = |grant: &RemoteGrant| grant.matches(target); if self.initial.js_remote.permits(matches) && state.js_remote.permits(matches) { if let Some(parent) = &self.parent { return parent.check_remote_exec(target); } return Ok(()); } Err(CapabilitiesError::RemoteExecDenied(target.to_string())) } pub fn check_js_exec(&self, target: &PathTarget) -> Result<(), CapabilitiesError> { let path = target.as_path(); let state = self.restrictions.read().unwrap(); let matches = |grant: &PathGrant| grant.matches(path); let read = self.initial.read.permits(matches) && state.read.permits(matches); let exec = self.initial.js_exec.granted(matches) || read; if exec && self.initial.js_exec.bounded(matches) && state.js_exec.bounded(matches) { if let Some(parent) = &self.parent { return parent.check_js_exec(target); } return Ok(()); } Err(CapabilitiesError::JsExecDenied(path.to_owned())) } }