char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somrearrange libs into lib and extensions into extsf4a0f5b

main
43.4 KiB1207 linesraw
1pub mod fs;
2
3use std::borrow::Cow;
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, RwLock};
6
7#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
8pub enum CapabilitiesError {
9    #[error("Unknown capability: {0}")]
10    UnknownCapability(String),
11    #[error("Unknown system information name: {0}")]
12    UnknownSys(String),
13    #[error("Environment variable names must be nonempty and contain neither NUL nor '='")]
14    InvalidEnvName,
15    #[error("Invalid network grant: {0}")]
16    InvalidNetGrant(String),
17    #[error("Invalid execution URL: {0}")]
18    InvalidExecUrl(String),
19    #[error("Invalid path grant: {0}")]
20    InvalidPathGrant(String),
21    #[error("Invalid file URL grant: {0}")]
22    InvalidFileUrlGrant(String),
23    #[error("File URL grants cannot contain queries or fragments")]
24    FileUrlQueryFragment,
25    #[error("Invalid empty or NUL-containing path grant")]
26    EmptyPathGrant,
27    #[error("Encoded path separators are not supported")]
28    EncodedSeparator,
29    #[error("Remote paths must be UTF-8")]
30    RemoteNotUtf8,
31    #[error("Expected a hierarchical module URL")]
32    NotHierarchical,
33    #[error("Invalid remote path segment")]
34    InvalidRemoteSegment,
35    #[error("Network checks require a host and separate port")]
36    NetworkRequiresHostPort,
37    #[error("PermissionDenied: env {0}")]
38    EnvDenied(String),
39    #[error("PermissionDenied: sys {0}")]
40    SysDenied(String),
41    #[error("PermissionDenied: net {0}:{1}")]
42    NetDenied(String, u16),
43    #[error("PermissionDenied: net {0}")]
44    NetUrlDenied(String),
45    #[error("PermissionDenied: read {}", .0.display())]
46    ReadDenied(PathBuf),
47    #[error("PermissionDenied: write {}", .0.display())]
48    WriteDenied(PathBuf),
49    #[error("PermissionDenied: run {}", .0.display())]
50    RunDenied(PathBuf),
51    #[error("PermissionDenied: ffi {}", .0.display())]
52    FfiDenied(PathBuf),
53    #[error("PermissionDenied: ffi")]
54    FfiDeniedAny,
55    #[error("PermissionDenied: read tree {}", .0.display())]
56    ReadTreeDenied(PathBuf),
57    #[error("PermissionDenied: write tree {}", .0.display())]
58    WriteTreeDenied(PathBuf),
59    #[error("PermissionDenied: js-exec {}", .0.display())]
60    JsExecDenied(PathBuf),
61    #[error("PermissionDenied: js-exec {0}")]
62    RemoteAcquireDenied(String),
63    #[error("PermissionDenied: js-exec {0}")]
64    RemoteExecDenied(String),
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum Category {
69    Net,
70    Read,
71    Write,
72    JsExec,
73    Env,
74    Sys,
75    Run,
76    Ffi,
77}
78
79// The CLI spells one name js-exec and the JS pledge API spells it jsExec.
80impl std::str::FromStr for Category {
81    type Err = CapabilitiesError;
82
83    fn from_str(value: &str) -> Result<Self, Self::Err> {
84        Ok(match value {
85            "net" => Self::Net,
86            "read" => Self::Read,
87            "write" => Self::Write,
88            "js-exec" | "jsExec" => Self::JsExec,
89            "env" => Self::Env,
90            "sys" => Self::Sys,
91            "run" => Self::Run,
92            "ffi" => Self::Ffi,
93            _ => return Err(CapabilitiesError::UnknownCapability(value.to_owned())),
94        })
95    }
96}
97
98pub fn sys_name(value: &str) -> Result<&str, CapabilitiesError> {
99    match value {
100        "hostname" | "osRelease" | "uptime" | "loadavg" => Ok(value),
101        _ => Err(CapabilitiesError::UnknownSys(value.to_owned())),
102    }
103}
104
105pub fn env_name(value: &str) -> Result<Cow<'_, str>, CapabilitiesError> {
106    if value.is_empty() || value.contains(['\0', '=']) {
107        return Err(CapabilitiesError::InvalidEnvName);
108    }
109    if cfg!(windows) {
110        Ok(Cow::Owned(value.to_ascii_uppercase()))
111    } else {
112        Ok(Cow::Borrowed(value))
113    }
114}
115
116#[derive(Clone, Debug, PartialEq, Eq)]
117pub struct NetGrant {
118    host: url::Host<String>,
119    port: Option<u16>,
120}
121
122impl NetGrant {
123    pub fn parse(value: &str) -> Result<Self, CapabilitiesError> {
124        let invalid = || CapabilitiesError::InvalidNetGrant(value.to_owned());
125        if value.is_empty()
126            || value
127                .chars()
128                .any(|c| c.is_whitespace() || ",/@?#\\%*".contains(c))
129        {
130            return Err(invalid());
131        }
132        let (host, port) = value
133            .rsplit_once(':')
134            .filter(|_| !value.ends_with(']'))
135            .map_or((value, None), |(host, port)| (host, Some(port)));
136        let port = port
137            .map(|port| {
138                if port.is_empty() || !port.bytes().all(|c| c.is_ascii_digit()) {
139                    return Err(invalid());
140                }
141                port.parse::<u16>().map_err(|_| invalid())
142            })
143            .transpose()?;
144        let mut host = url::Host::parse(host).map_err(|_| invalid())?;
145        if let url::Host::Domain(name) = &mut host {
146            if name.ends_with('.') {
147                name.pop();
148            }
149            if name.is_empty() {
150                return Err(invalid());
151            }
152        }
153        Ok(Self { host, port })
154    }
155
156    fn matches(&self, target: &Self) -> bool {
157        self.host == target.host && (self.port.is_none() || self.port == target.port)
158    }
159}
160
161pub fn canonical_remote_url(url: &url::Url) -> Result<url::Url, CapabilitiesError> {
162    let lower = url.path().to_ascii_lowercase();
163    if lower.contains("%2f") || lower.contains("%5c") {
164        return Err(CapabilitiesError::EncodedSeparator);
165    }
166    let decoded = percent_encoding::percent_decode_str(url.path())
167        .decode_utf8()
168        .map_err(|_| CapabilitiesError::RemoteNotUtf8)?;
169    let mut canonical = url.clone();
170    {
171        let mut segments = canonical
172            .path_segments_mut()
173            .map_err(|_| CapabilitiesError::NotHierarchical)?;
174        segments.clear();
175        for segment in decoded.strip_prefix('/').unwrap_or(&decoded).split('/') {
176            if matches!(segment, "." | "..")
177                || segment.contains('\\')
178                || segment.chars().any(char::is_control)
179            {
180                return Err(CapabilitiesError::InvalidRemoteSegment);
181            }
182            segments.push(segment);
183        }
184    }
185    Ok(canonical)
186}
187
188#[derive(Clone, Debug)]
189pub struct RemoteGrant {
190    url: url::Url,
191    subtree: bool,
192}
193
194impl RemoteGrant {
195    fn parse(value: &str) -> Result<Self, CapabilitiesError> {
196        let url = url::Url::parse(value)
197            .map_err(|_| CapabilitiesError::InvalidExecUrl(value.to_owned()))?;
198        if !matches!(url.scheme(), "http" | "https")
199            || url.host().is_none()
200            || !url.username().is_empty()
201            || url.password().is_some()
202            || url.query().is_some()
203            || url.fragment().is_some()
204        {
205            return Err(CapabilitiesError::InvalidExecUrl(value.to_owned()));
206        }
207        let url = canonical_remote_url(&url)?;
208        let subtree = url.path().ends_with('/');
209        Ok(Self { url, subtree })
210    }
211
212    fn overlaps(&self, base: &url::Url) -> bool {
213        self.matches(base)
214            || (self.url.origin() == base.origin() && self.url.path().starts_with(base.path()))
215    }
216
217    fn matches(&self, target: &url::Url) -> bool {
218        self.url.origin() == target.origin()
219            && (self.url.path() == target.path()
220                || self.subtree
221                    && if self.url.path().ends_with('/') {
222                        target.path().starts_with(self.url.path())
223                    } else {
224                        target.path().starts_with(&format!("{}/", self.url.path()))
225                    })
226    }
227}
228
229fn remote_execution(value: &str) -> bool {
230    value.split_once(':').is_some_and(|(scheme, _)| {
231        scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https")
232    })
233}
234
235#[derive(Clone, Debug)]
236pub struct PathTarget(PathBuf);
237
238impl PathTarget {
239    pub fn resolve(path: impl AsRef<Path>) -> std::io::Result<Self> {
240        path.as_ref().canonicalize().map(Self)
241    }
242
243    /// Resolve the existing prefix of a possibly missing path. Opening must refuse
244    /// symlinks in every component, including dangling symlinks in the missing tail.
245    pub fn for_creation(path: impl AsRef<Path>) -> std::io::Result<Self> {
246        let path = if path.as_ref().is_absolute() {
247            path.as_ref().to_owned()
248        } else {
249            std::env::current_dir()?.join(path)
250        };
251        resolve_prefix(&path).map(|(path, _)| Self(path))
252    }
253
254    /// Resolve parents but preserve the final directory entry for no-follow metadata.
255    pub fn for_entry(path: impl AsRef<Path>) -> std::io::Result<Self> {
256        let path = path.as_ref();
257        match path.components().next_back() {
258            Some(std::path::Component::Normal(name)) => {
259                let parent = Self::for_creation(path.parent().unwrap_or_else(|| Path::new(".")))?;
260                Ok(Self(parent.into_path().join(name)))
261            }
262            _ => Self::for_creation(path),
263        }
264    }
265
266    /// Like `for_entry`, but an existing entry takes its stored spelling, as
267    /// canonicalization gives its parents, so case-insensitive filesystems
268    /// cannot respell an executable past a run grant or denial.
269    pub fn for_executable(path: impl AsRef<Path>) -> std::io::Result<Self> {
270        let Self(path) = Self::for_entry(path)?;
271        stored_spelling(path).map(Self)
272    }
273
274    pub fn child(&self, name: impl AsRef<std::ffi::OsStr>) -> std::io::Result<Self> {
275        let path = Path::new(name.as_ref());
276        let mut components = path.components();
277        if !matches!(components.next(), Some(std::path::Component::Normal(_)))
278            || components.next().is_some()
279        {
280            return Err(std::io::Error::new(
281                std::io::ErrorKind::InvalidInput,
282                "Expected a single entry name",
283            ));
284        }
285        Ok(Self(self.0.join(path)))
286    }
287
288    pub fn as_path(&self) -> &Path {
289        &self.0
290    }
291    pub fn into_path(self) -> PathBuf {
292        self.0
293    }
294}
295
296/// Lookup failures keep the lexical name: anything that stops this no-follow
297/// lookup also stops opening or executing the entry by that path.
298#[cfg(target_os = "macos")]
299fn stored_spelling(path: PathBuf) -> std::io::Result<PathBuf> {
300    use std::os::unix::ffi::{OsStrExt, OsStringExt};
301    use std::os::unix::fs::MetadataExt;
302    #[repr(C)]
303    struct Attributes {
304        length: u32,
305        name: libc::attrreference_t,
306        data: [u8; 1024],
307    }
308    let text = std::ffi::CString::new(path.as_os_str().as_bytes())?;
309    let mut request = libc::attrlist {
310        bitmapcount: libc::ATTR_BIT_MAP_COUNT,
311        reserved: 0,
312        commonattr: libc::ATTR_CMN_NAME,
313        volattr: 0,
314        dirattr: 0,
315        fileattr: 0,
316        forkattr: 0,
317    };
318    let mut attributes: Attributes = unsafe { std::mem::zeroed() };
319    if unsafe {
320        libc::getattrlist(
321            text.as_ptr(),
322            (&raw mut request).cast(),
323            (&raw mut attributes).cast(),
324            std::mem::size_of::<Attributes>(),
325            libc::FSOPT_NOFOLLOW,
326        )
327    } < 0
328    {
329        return Ok(path);
330    }
331    // The NUL-terminated name lies at an offset from its reference.
332    let start = (std::mem::offset_of!(Attributes, name) as isize
333        + attributes.name.attr_dataoffset as isize
334        - std::mem::offset_of!(Attributes, data) as isize) as usize;
335    let length = attributes.name.attr_length as usize;
336    let Some(name) = attributes
337        .data
338        .get(start..start.saturating_add(length))
339        .and_then(|name| name.strip_suffix(&[0]))
340    else {
341        return Err(std::io::Error::other("Malformed file name attribute"));
342    };
343    let stored = path.with_file_name(std::ffi::OsString::from_vec(name.to_vec()));
344    // A hard-linked file may report another link's name; keep it only if
345    // it names this same entry's file here.
346    let same = |path: &Path| path.symlink_metadata().map(|meta| (meta.dev(), meta.ino()));
347    if stored != path && same(&stored).ok() != same(&path).ok() {
348        return Ok(path);
349    }
350    Ok(stored)
351}
352
353#[cfg(windows)]
354fn stored_spelling(path: PathBuf) -> std::io::Result<PathBuf> {
355    use std::os::windows::ffi::{OsStrExt, OsStringExt};
356    use std::os::windows::fs::OpenOptionsExt;
357    use std::os::windows::io::AsRawHandle;
358    use windows_sys::Win32::Storage::FileSystem::{
359        FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_NAME_NORMALIZED,
360        FILE_READ_ATTRIBUTES, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
361        GetFinalPathNameByHandleW, VOLUME_NAME_NONE,
362    };
363    // A stream suffix names a different object than the entry.
364    let name = path.file_name().unwrap_or_default();
365    if name.encode_wide().any(|unit| unit == u16::from(b':')) {
366        return Err(std::io::Error::new(
367            std::io::ErrorKind::InvalidInput,
368            "Invalid file name",
369        ));
370    }
371    // Opening a known name needs no permission to list its directory, so a
372    // lookup that could fail where the launch succeeds must not stand in.
373    let file = match std::fs::OpenOptions::new()
374        .access_mode(FILE_READ_ATTRIBUTES)
375        .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
376        .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS)
377        .open(&path)
378    {
379        Ok(file) => file,
380        Err(error)
381            if matches!(
382                error.kind(),
383                std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
384            ) =>
385        {
386            return Ok(path);
387        }
388        Err(error) => return Err(error),
389    };
390    let mut buffer = vec![0u16; 512];
391    loop {
392        let length = unsafe {
393            GetFinalPathNameByHandleW(
394                file.as_raw_handle(),
395                buffer.as_mut_ptr(),
396                buffer.len() as u32,
397                FILE_NAME_NORMALIZED | VOLUME_NAME_NONE,
398            )
399        } as usize;
400        if length == 0 {
401            return Err(std::io::Error::last_os_error());
402        }
403        if length < buffer.len() {
404            buffer.truncate(length);
405            break;
406        }
407        buffer.resize(length, 0);
408    }
409    // The long name as stored, also undoing 8.3 short names.
410    let stored = PathBuf::from(std::ffi::OsString::from_wide(&buffer));
411    let name = stored
412        .file_name()
413        .ok_or_else(|| std::io::Error::other("Entry has no stored name"))?;
414    Ok(path.with_file_name(name))
415}
416
417#[cfg(not(any(target_os = "macos", windows)))]
418fn stored_spelling(path: PathBuf) -> std::io::Result<PathBuf> {
419    Ok(path)
420}
421
422fn resolve_prefix(path: &Path) -> std::io::Result<(PathBuf, bool)> {
423    let mut input = path.to_owned();
424    let mut redirects = 0;
425    'resolve: loop {
426        let mut ancestor = input.as_path();
427        let mut tail = Vec::new();
428        let mut resolved = loop {
429            match ancestor.canonicalize() {
430                Ok(path) => break path,
431                Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
432                    // A dangling link is an existing entry, not a missing lexical tail.
433                    match ancestor.symlink_metadata() {
434                        Ok(metadata) if metadata.is_symlink() => {
435                            if redirects == 40 {
436                                return Err(std::io::Error::other(
437                                    "Too many symbolic links while resolving path",
438                                ));
439                            }
440                            let contents = ancestor.read_link()?;
441                            let mut target = ancestor.parent().ok_or(error)?.join(contents);
442                            for name in tail.into_iter().rev() {
443                                target.push(name);
444                            }
445                            input = target;
446                            redirects += 1;
447                            continue 'resolve;
448                        }
449                        Err(error) if error.kind() != std::io::ErrorKind::NotFound => {
450                            return Err(error);
451                        }
452                        _ => (),
453                    }
454                    let Some(std::path::Component::Normal(name)) =
455                        ancestor.components().next_back()
456                    else {
457                        return Err(std::io::Error::new(
458                            std::io::ErrorKind::NotFound,
459                            "Missing parent cannot resolve path components",
460                        ));
461                    };
462                    tail.push(name);
463                    ancestor = ancestor.parent().ok_or(error)?;
464                }
465                Err(error) => return Err(error),
466            }
467        };
468        let existing = tail.is_empty();
469        for name in tail.into_iter().rev() {
470            resolved.push(name);
471        }
472        return Ok((resolved, existing));
473    }
474}
475
476#[derive(Clone, Debug)]
477pub struct PathGrant {
478    path: PathBuf,
479    directory: bool,
480}
481
482impl PathGrant {
483    pub fn parse(value: &str, cwd: &Path) -> Result<Self, CapabilitiesError> {
484        Self::parse_with_directory(value, cwd, true, false)
485    }
486
487    fn parse_file(value: &str, cwd: &Path) -> Result<Self, CapabilitiesError> {
488        let directory = value.ends_with('/') || (cfg!(windows) && value.ends_with('\\'));
489        Self::parse_with_directory(value, cwd, directory, false)
490    }
491
492    fn parse_run(value: &str, cwd: &Path) -> Result<Self, CapabilitiesError> {
493        let directory = value.ends_with('/') || (cfg!(windows) && value.ends_with('\\'));
494        Self::parse_with_directory(value, cwd, directory, true)
495    }
496
497    fn parse_with_directory(
498        value: &str,
499        cwd: &Path,
500        missing_directory: bool,
501        executable_entry: bool,
502    ) -> Result<Self, CapabilitiesError> {
503        if value.is_empty() || value.contains('\0') {
504            return Err(CapabilitiesError::EmptyPathGrant);
505        }
506        let path = if value
507            .get(..5)
508            .is_some_and(|scheme| scheme.eq_ignore_ascii_case("file:"))
509        {
510            let url = url::Url::parse(value)
511                .map_err(|_| CapabilitiesError::InvalidFileUrlGrant(value.to_owned()))?;
512            if url.query().is_some() || url.fragment().is_some() {
513                return Err(CapabilitiesError::FileUrlQueryFragment);
514            }
515            url.to_file_path()
516                .map_err(|_| CapabilitiesError::InvalidFileUrlGrant(value.to_owned()))?
517        } else {
518            cwd.join(value)
519        };
520        // Multi-call executables must not turn one granted entry into authority for all aliases.
521        if executable_entry && !missing_directory && !path.is_dir() {
522            return Ok(Self {
523                path: PathTarget::for_executable(&path)
524                    .map_err(|_| CapabilitiesError::InvalidPathGrant(value.to_owned()))?
525                    .into_path(),
526                directory: false,
527            });
528        }
529        // Resolve existing symlinks, including the existing prefix of a not-yet-created path.
530        let (path, existing) = resolve_prefix(&path)
531            .map_err(|_| CapabilitiesError::InvalidPathGrant(value.to_owned()))?;
532        let directory = if existing {
533            path.is_dir()
534        } else {
535            missing_directory
536        };
537        Ok(Self { path, directory })
538    }
539
540    fn matches(&self, target: &Path) -> bool {
541        if self.directory {
542            target.starts_with(&self.path)
543        } else {
544            target == self.path
545        }
546    }
547}
548
549#[derive(Clone, Debug)]
550struct Policy<G> {
551    all: bool,
552    allow: Vec<G>,
553    deny: Vec<G>,
554    retain: Vec<Vec<G>>,
555}
556
557impl<G> Default for Policy<G> {
558    fn default() -> Self {
559        Self {
560            all: true,
561            allow: Vec::new(),
562            deny: Vec::new(),
563            retain: Vec::new(),
564        }
565    }
566}
567
568impl<G> Policy<G> {
569    fn granted(&self, matches: impl Fn(&G) -> bool) -> bool {
570        self.all || self.allow.iter().any(matches)
571    }
572    fn bounded(&self, matches: impl Fn(&G) -> bool) -> bool {
573        !self.deny.iter().any(&matches) && self.retain.iter().all(|list| list.iter().any(&matches))
574    }
575    fn permits(&self, matches: impl Fn(&G) -> bool) -> bool {
576        self.granted(&matches) && self.bounded(matches)
577    }
578    fn restrict(&mut self, allow: Option<Vec<G>>, deny: Vec<G>) {
579        if let Some(allow) = allow {
580            self.retain.push(allow);
581        }
582        self.deny.extend(deny);
583    }
584}
585
586#[derive(Clone, Default)]
587pub struct Initial {
588    env: Policy<String>,
589    sys: Policy<String>,
590    net: Policy<NetGrant>,
591    read: Policy<PathGrant>,
592    write: Policy<PathGrant>,
593    js_exec: Policy<PathGrant>,
594    run: Policy<PathGrant>,
595    ffi: Policy<PathGrant>,
596    js_remote: Policy<RemoteGrant>,
597}
598
599impl Initial {
600    pub fn new(zero: bool) -> Self {
601        let mut initial = Self::default();
602        initial.env.all = !zero;
603        initial.sys.all = !zero;
604        initial.net.all = !zero;
605        initial.read.all = !zero;
606        initial.write.all = !zero;
607        initial.js_exec.all = !zero;
608        initial.run.all = !zero;
609        initial.ffi.all = !zero;
610        initial.js_remote.all = !zero;
611        initial
612    }
613
614    pub fn grant(
615        &mut self,
616        category: Category,
617        deny: bool,
618        value: &str,
619        cwd: &Path,
620    ) -> Result<(), CapabilitiesError> {
621        if category == Category::JsExec && remote_execution(value) {
622            let mut grant = RemoteGrant::parse(value)?;
623            if deny {
624                grant.subtree = true;
625                self.js_remote.deny.push(grant);
626            } else {
627                self.js_remote.allow.push(grant);
628            }
629            return Ok(());
630        }
631        match category {
632            Category::Env => {
633                let name = env_name(value)?.into_owned();
634                if deny {
635                    self.env.deny.push(name);
636                } else {
637                    self.env.allow.push(name);
638                }
639            }
640            Category::Sys => {
641                let name = sys_name(value)?.to_owned();
642                if deny {
643                    self.sys.deny.push(name);
644                } else {
645                    self.sys.allow.push(name);
646                }
647            }
648            Category::Net => {
649                let grant = NetGrant::parse(value)?;
650                if deny {
651                    self.net.deny.push(grant);
652                } else {
653                    self.net.allow.push(grant);
654                }
655            }
656            Category::Read | Category::Write | Category::JsExec | Category::Run | Category::Ffi => {
657                let mut grant = if matches!(category, Category::Run | Category::Ffi) {
658                    PathGrant::parse_run(value, cwd)?
659                } else if category == Category::Write {
660                    PathGrant::parse_file(value, cwd)?
661                } else {
662                    PathGrant::parse(value, cwd)?
663                };
664                let policy = match category {
665                    Category::Read => &mut self.read,
666                    Category::Write => &mut self.write,
667                    Category::Run => &mut self.run,
668                    Category::Ffi => &mut self.ffi,
669                    _ => &mut self.js_exec,
670                };
671                if deny {
672                    grant.directory = true;
673                    policy.deny.push(grant);
674                } else {
675                    policy.allow.push(grant);
676                }
677            }
678        }
679        Ok(())
680    }
681
682    pub fn remote_entry(&mut self, url: &url::Url) -> Result<(), CapabilitiesError> {
683        self.js_remote.allow.push(RemoteGrant {
684            url: canonical_remote_url(url)?,
685            subtree: false,
686        });
687        Ok(())
688    }
689
690    pub fn entry(&mut self, path: &Path) {
691        self.js_exec.allow.push(PathGrant {
692            path: path.to_owned(),
693            directory: false,
694        });
695    }
696}
697
698// Stored parents are immutable snapshots, never live ceilings.
699pub struct Capabilities {
700    initial: Initial,
701    restrictions: RwLock<Initial>,
702    parent: Option<Arc<Self>>,
703    cwd: PathBuf,
704}
705
706pub type Pledge = Vec<(Category, Option<Vec<String>>, Vec<String>)>;
707
708enum Restriction {
709    Env(Option<Vec<String>>, Vec<String>),
710    Sys(Option<Vec<String>>, Vec<String>),
711    Net(Option<Vec<NetGrant>>, Vec<NetGrant>),
712    Read(Option<Vec<PathGrant>>, Vec<PathGrant>),
713    Write(Option<Vec<PathGrant>>, Vec<PathGrant>),
714    JsExec(Option<Vec<PathGrant>>, Vec<PathGrant>),
715    Run(Option<Vec<PathGrant>>, Vec<PathGrant>),
716    Ffi(Option<Vec<PathGrant>>, Vec<PathGrant>),
717    JsRemote(Option<Vec<RemoteGrant>>, Vec<RemoteGrant>),
718}
719
720impl Capabilities {
721    pub fn new(initial: Initial, cwd: PathBuf) -> Arc<Self> {
722        Arc::new(Self {
723            initial,
724            restrictions: RwLock::new(Initial::default()),
725            parent: None,
726            cwd,
727        })
728    }
729
730    pub fn inherit(parent: Arc<Self>) -> Arc<Self> {
731        // The chain above the parent is already frozen, so cloning this policy
732        // under one lock and sharing the chain is a complete atomic snapshot.
733        let snapshot = Self {
734            initial: parent.initial.clone(),
735            restrictions: RwLock::new(parent.restrictions.read().unwrap().clone()),
736            parent: parent.parent.clone(),
737            cwd: parent.cwd.clone(),
738        };
739        Arc::new(Self {
740            initial: Initial::default(),
741            restrictions: RwLock::new(Initial::default()),
742            cwd: parent.cwd.clone(),
743            parent: Some(Arc::new(snapshot)),
744        })
745    }
746
747    pub fn pledge(&self, request: Pledge) -> Result<(), CapabilitiesError> {
748        let mut prepared = Vec::new();
749        for (category, allow, deny) in request {
750            let restriction = match category {
751                Category::Sys => Restriction::Sys(
752                    allow
753                        .map(|values| {
754                            values
755                                .iter()
756                                .map(|s| sys_name(s).map(str::to_owned))
757                                .collect()
758                        })
759                        .transpose()?,
760                    deny.iter()
761                        .map(|s| sys_name(s).map(str::to_owned))
762                        .collect::<Result<_, _>>()?,
763                ),
764                Category::Env => Restriction::Env(
765                    allow
766                        .map(|values| {
767                            values
768                                .iter()
769                                .map(|s| env_name(s).map(Cow::into_owned))
770                                .collect()
771                        })
772                        .transpose()?,
773                    deny.iter()
774                        .map(|s| env_name(s).map(Cow::into_owned))
775                        .collect::<Result<_, _>>()?,
776                ),
777                Category::Net => Restriction::Net(
778                    allow
779                        .map(|values| {
780                            values
781                                .iter()
782                                .map(|s| NetGrant::parse(s))
783                                .collect::<Result<Vec<_>, _>>()
784                        })
785                        .transpose()?,
786                    deny.iter()
787                        .map(|s| NetGrant::parse(s))
788                        .collect::<Result<Vec<_>, _>>()?,
789                ),
790                Category::JsExec => {
791                    let parse = |values: Vec<String>| {
792                        let mut paths = Vec::new();
793                        let mut remote = Vec::new();
794                        for value in values {
795                            if remote_execution(&value) {
796                                remote.push(RemoteGrant::parse(&value)?);
797                            } else {
798                                paths.push(PathGrant::parse(&value, &self.cwd)?);
799                            }
800                        }
801                        Ok::<_, CapabilitiesError>((paths, remote))
802                    };
803                    let allow = allow.map(parse).transpose()?;
804                    let (mut paths, mut remote) = parse(deny)?;
805                    for grant in &mut paths {
806                        grant.directory = true;
807                    }
808                    for grant in &mut remote {
809                        grant.subtree = true;
810                    }
811                    prepared.push(Restriction::JsExec(
812                        allow.as_ref().map(|(paths, _)| paths.clone()),
813                        paths,
814                    ));
815                    Restriction::JsRemote(allow.map(|(_, remote)| remote), remote)
816                }
817                Category::Read | Category::Write | Category::Run | Category::Ffi => {
818                    let parse = |values: Vec<String>| {
819                        values
820                            .iter()
821                            .map(|s| {
822                                if matches!(category, Category::Run | Category::Ffi) {
823                                    PathGrant::parse_run(s, &self.cwd)
824                                } else if category == Category::Write {
825                                    PathGrant::parse_file(s, &self.cwd)
826                                } else {
827                                    PathGrant::parse(s, &self.cwd)
828                                }
829                            })
830                            .collect::<Result<Vec<_>, _>>()
831                    };
832                    let allow = allow.map(parse).transpose()?;
833                    let mut deny = parse(deny)?;
834                    for grant in &mut deny {
835                        grant.directory = true;
836                    }
837                    match category {
838                        Category::Read => Restriction::Read(allow, deny),
839                        Category::Write => Restriction::Write(allow, deny),
840                        Category::Run => Restriction::Run(allow, deny),
841                        Category::Ffi => Restriction::Ffi(allow, deny),
842                        _ => unreachable!(),
843                    }
844                }
845            };
846            prepared.push(restriction);
847        }
848        let mut state = self.restrictions.write().unwrap();
849        for restriction in prepared {
850            match restriction {
851                Restriction::Env(allow, deny) => state.env.restrict(allow, deny),
852                Restriction::Sys(allow, deny) => state.sys.restrict(allow, deny),
853                Restriction::Net(allow, deny) => state.net.restrict(allow, deny),
854                Restriction::Read(allow, deny) => state.read.restrict(allow, deny),
855                Restriction::Write(allow, deny) => state.write.restrict(allow, deny),
856                Restriction::JsExec(allow, deny) => state.js_exec.restrict(allow, deny),
857                Restriction::Run(allow, deny) => state.run.restrict(allow, deny),
858                Restriction::Ffi(allow, deny) => state.ffi.restrict(allow, deny),
859                Restriction::JsRemote(allow, deny) => state.js_remote.restrict(allow, deny),
860            }
861        }
862        Ok(())
863    }
864
865    pub fn check_run(&self, target: &PathTarget) -> Result<(), CapabilitiesError> {
866        let matches = |grant: &PathGrant| grant.matches(target.as_path());
867        let state = self.restrictions.read().unwrap();
868        if self.initial.run.permits(matches) && state.run.permits(matches) {
869            if let Some(parent) = &self.parent {
870                return parent.check_run(target);
871            }
872            return Ok(());
873        }
874        Err(CapabilitiesError::RunDenied(target.as_path().to_owned()))
875    }
876
877    pub fn check_ffi(&self, target: &PathTarget) -> Result<(), CapabilitiesError> {
878        let matches = |grant: &PathGrant| grant.matches(target.as_path());
879        let state = self.restrictions.read().unwrap();
880        if self.initial.ffi.permits(matches) && state.ffi.permits(matches) {
881            if let Some(parent) = &self.parent {
882                return parent.check_ffi(target);
883            }
884            return Ok(());
885        }
886        Err(CapabilitiesError::FfiDenied(target.as_path().to_owned()))
887    }
888
889    /// Whether any ffi authority remains at all. Path grants count: holding
890    /// authority to load a specific library is enough to use pointer and
891    /// function-pointer operations, which name no path of their own. This
892    /// looks for a witness path that every layer (initial grants, pledges,
893    /// and parents) would permit, so deny rules and disjoint retain lists
894    /// count as withholding authority.
895    pub fn check_ffi_any(&self) -> Result<(), CapabilitiesError> {
896        let mut policies = vec![self.initial.ffi.clone()];
897        {
898            let state = self.restrictions.read().unwrap();
899            policies.push(state.ffi.clone());
900        }
901        if let Some(parent) = &self.parent {
902            parent.collect_ffi_policies(&mut policies);
903        }
904        // Grant spellings cannot contain NUL, so only a root deny covers this.
905        let mut witnesses = vec![PathBuf::from("/\0")];
906        for policy in &policies {
907            witnesses.extend(policy.allow.iter().map(|grant| grant.path.clone()));
908            witnesses.extend(
909                policy
910                    .retain
911                    .iter()
912                    .flatten()
913                    .map(|grant| grant.path.clone()),
914            );
915        }
916        let permitted = witnesses.iter().any(|witness| {
917            policies
918                .iter()
919                .all(|policy| policy.permits(|grant| grant.matches(witness)))
920        });
921        if permitted {
922            Ok(())
923        } else {
924            Err(CapabilitiesError::FfiDeniedAny)
925        }
926    }
927
928    fn collect_ffi_policies(&self, policies: &mut Vec<Policy<PathGrant>>) {
929        policies.push(self.initial.ffi.clone());
930        {
931            let state = self.restrictions.read().unwrap();
932            policies.push(state.ffi.clone());
933        }
934        if let Some(parent) = &self.parent {
935            parent.collect_ffi_policies(policies);
936        }
937    }
938
939    pub fn unrestricted_env(&self) -> bool {
940        let state = self.restrictions.read().unwrap();
941        self.initial.env.all
942            && self.initial.env.deny.is_empty()
943            && self.initial.env.retain.is_empty()
944            && state.env.deny.is_empty()
945            && state.env.retain.is_empty()
946            && self
947                .parent
948                .as_ref()
949                .is_none_or(|parent| parent.unrestricted_env())
950    }
951
952    pub fn check_env(&self, name: &str) -> Result<(), CapabilitiesError> {
953        self.check_env_name(&env_name(name)?)
954    }
955
956    fn check_env_name(&self, name: &str) -> Result<(), CapabilitiesError> {
957        let state = self.restrictions.read().unwrap();
958        let matches = |grant: &String| grant == name;
959        if self.initial.env.permits(matches) && state.env.permits(matches) {
960            if let Some(parent) = &self.parent {
961                return parent.check_env_name(name);
962            }
963            return Ok(());
964        }
965        Err(CapabilitiesError::EnvDenied(name.to_owned()))
966    }
967
968    pub fn check_sys(&self, name: &str) -> Result<(), CapabilitiesError> {
969        self.check_sys_name(sys_name(name)?)
970    }
971
972    fn check_sys_name(&self, name: &str) -> Result<(), CapabilitiesError> {
973        let state = self.restrictions.read().unwrap();
974        let matches = |grant: &String| grant == name;
975        if self.initial.sys.permits(matches) && state.sys.permits(matches) {
976            if let Some(parent) = &self.parent {
977                return parent.check_sys_name(name);
978            }
979            return Ok(());
980        }
981        Err(CapabilitiesError::SysDenied(name.to_owned()))
982    }
983
984    pub fn check_net(&self, host: &str, port: u16) -> Result<(), CapabilitiesError> {
985        let mut target = match host.parse::<std::net::Ipv6Addr>() {
986            Ok(ip) => NetGrant {
987                host: url::Host::Ipv6(ip),
988                port: None,
989            },
990            Err(_) => NetGrant::parse(host)?,
991        };
992        if target.port.is_some() {
993            return Err(CapabilitiesError::NetworkRequiresHostPort);
994        }
995        target.port = Some(port);
996        if self.permits_net(&target) {
997            Ok(())
998        } else {
999            Err(CapabilitiesError::NetDenied(host.to_owned(), port))
1000        }
1001    }
1002
1003    fn permits_net(&self, target: &NetGrant) -> bool {
1004        let state = self.restrictions.read().unwrap();
1005        let matches = |grant: &NetGrant| grant.matches(target);
1006        self.initial.net.permits(matches)
1007            && state.net.permits(matches)
1008            && self
1009                .parent
1010                .as_ref()
1011                .is_none_or(|parent| parent.permits_net(target))
1012    }
1013
1014    pub fn check_read(&self, target: &PathTarget) -> Result<(), CapabilitiesError> {
1015        let path = target.as_path();
1016        let state = self.restrictions.read().unwrap();
1017        let matches = |grant: &PathGrant| grant.matches(path);
1018        if self.initial.read.permits(matches) && state.read.permits(matches) {
1019            if let Some(parent) = &self.parent {
1020                return parent.check_read(target);
1021            }
1022            return Ok(());
1023        }
1024        Err(CapabilitiesError::ReadDenied(path.to_owned()))
1025    }
1026
1027    pub fn cwd(&self) -> &Path {
1028        &self.cwd
1029    }
1030
1031    pub fn check_write(&self, target: &PathTarget) -> Result<(), CapabilitiesError> {
1032        let path = target.as_path();
1033        let state = self.restrictions.read().unwrap();
1034        let matches = |grant: &PathGrant| grant.matches(path);
1035        if self.initial.write.permits(matches) && state.write.permits(matches) {
1036            if let Some(parent) = &self.parent {
1037                return parent.check_write(target);
1038            }
1039            return Ok(());
1040        }
1041        Err(CapabilitiesError::WriteDenied(path.to_owned()))
1042    }
1043
1044    pub fn check_read_tree(&self, target: &PathTarget) -> Result<(), CapabilitiesError> {
1045        self.check_tree(target, false)
1046    }
1047
1048    pub fn check_write_tree(&self, target: &PathTarget) -> Result<(), CapabilitiesError> {
1049        self.check_tree(target, true)
1050    }
1051
1052    fn check_tree(&self, target: &PathTarget, write: bool) -> Result<(), CapabilitiesError> {
1053        let root = target.as_path();
1054        let permits = |policy: &Policy<PathGrant>| {
1055            let covers = |grant: &PathGrant| grant.directory && root.starts_with(&grant.path);
1056            (policy.all || policy.allow.iter().any(covers))
1057                && policy.retain.iter().all(|list| list.iter().any(covers))
1058                && !policy
1059                    .deny
1060                    .iter()
1061                    .any(|grant| root.starts_with(&grant.path) || grant.path.starts_with(root))
1062        };
1063        let state = self.restrictions.read().unwrap();
1064        let initial = if write {
1065            &self.initial.write
1066        } else {
1067            &self.initial.read
1068        };
1069        let restriction = if write { &state.write } else { &state.read };
1070        if permits(initial)
1071            && permits(restriction)
1072            && self
1073                .parent
1074                .as_ref()
1075                .is_none_or(|parent| parent.check_tree(target, write).is_ok())
1076        {
1077            Ok(())
1078        } else {
1079            Err(if write {
1080                CapabilitiesError::WriteTreeDenied(root.to_owned())
1081            } else {
1082                CapabilitiesError::ReadTreeDenied(root.to_owned())
1083            })
1084        }
1085    }
1086
1087    pub fn unrestricted_run(&self) -> bool {
1088        let state = self.restrictions.read().unwrap();
1089        self.initial.run.all
1090            && self.initial.run.deny.is_empty()
1091            && self.initial.run.retain.is_empty()
1092            && state.run.deny.is_empty()
1093            && state.run.retain.is_empty()
1094            && self
1095                .parent
1096                .as_ref()
1097                .is_none_or(|parent| parent.unrestricted_run())
1098    }
1099
1100    pub fn unrestricted_ffi(&self) -> bool {
1101        let state = self.restrictions.read().unwrap();
1102        self.initial.ffi.all
1103            && self.initial.ffi.deny.is_empty()
1104            && self.initial.ffi.retain.is_empty()
1105            && state.ffi.deny.is_empty()
1106            && state.ffi.retain.is_empty()
1107            && self
1108                .parent
1109                .as_ref()
1110                .is_none_or(|parent| parent.unrestricted_ffi())
1111    }
1112
1113    pub fn unrestricted_read(&self) -> bool {
1114        self.unrestricted_path(false)
1115    }
1116
1117    pub fn unrestricted_write(&self) -> bool {
1118        self.unrestricted_path(true)
1119    }
1120
1121    fn unrestricted_path(&self, write: bool) -> bool {
1122        let state = self.restrictions.read().unwrap();
1123        let initial = if write {
1124            &self.initial.write
1125        } else {
1126            &self.initial.read
1127        };
1128        let restriction = if write { &state.write } else { &state.read };
1129        initial.all
1130            && initial.deny.is_empty()
1131            && initial.retain.is_empty()
1132            && restriction.deny.is_empty()
1133            && restriction.retain.is_empty()
1134            && self
1135                .parent
1136                .as_ref()
1137                .is_none_or(|parent| parent.unrestricted_path(write))
1138    }
1139
1140    pub fn unrestricted_js_exec(&self) -> bool {
1141        let state = self.restrictions.read().unwrap();
1142        let unrestricted = self.initial.js_exec.deny.is_empty()
1143            && self.initial.js_exec.retain.is_empty()
1144            && state.js_exec.deny.is_empty()
1145            && state.js_exec.retain.is_empty()
1146            && (self.initial.js_exec.all
1147                || (self.initial.read.all
1148                    && self.initial.read.deny.is_empty()
1149                    && state.read.deny.is_empty()
1150                    && state.read.retain.is_empty()));
1151        unrestricted
1152            && self
1153                .parent
1154                .as_ref()
1155                .is_none_or(|parent| parent.unrestricted_js_exec())
1156    }
1157
1158    pub fn check_remote_acquire(&self, package: &url::Url) -> Result<(), CapabilitiesError> {
1159        let canonical = canonical_remote_url(package)?;
1160        let package = &canonical;
1161        let state = self.restrictions.read().unwrap();
1162        let potential = |policy: &Policy<RemoteGrant>| {
1163            (policy.all || policy.allow.iter().any(|grant| grant.overlaps(package)))
1164                && !policy.deny.iter().any(|grant| grant.matches(package))
1165                && policy
1166                    .retain
1167                    .iter()
1168                    .all(|list| list.iter().any(|grant| grant.overlaps(package)))
1169        };
1170        if potential(&self.initial.js_remote) && potential(&state.js_remote) {
1171            if let Some(parent) = &self.parent {
1172                return parent.check_remote_acquire(package);
1173            }
1174            return Ok(());
1175        }
1176        Err(CapabilitiesError::RemoteAcquireDenied(package.to_string()))
1177    }
1178
1179    pub fn check_remote_exec(&self, target: &url::Url) -> Result<(), CapabilitiesError> {
1180        let canonical = canonical_remote_url(target)?;
1181        let target = &canonical;
1182        let state = self.restrictions.read().unwrap();
1183        let matches = |grant: &RemoteGrant| grant.matches(target);
1184        if self.initial.js_remote.permits(matches) && state.js_remote.permits(matches) {
1185            if let Some(parent) = &self.parent {
1186                return parent.check_remote_exec(target);
1187            }
1188            return Ok(());
1189        }
1190        Err(CapabilitiesError::RemoteExecDenied(target.to_string()))
1191    }
1192
1193    pub fn check_js_exec(&self, target: &PathTarget) -> Result<(), CapabilitiesError> {
1194        let path = target.as_path();
1195        let state = self.restrictions.read().unwrap();
1196        let matches = |grant: &PathGrant| grant.matches(path);
1197        let read = self.initial.read.permits(matches) && state.read.permits(matches);
1198        let exec = self.initial.js_exec.granted(matches) || read;
1199        if exec && self.initial.js_exec.bounded(matches) && state.js_exec.bounded(matches) {
1200            if let Some(parent) = &self.parent {
1201                return parent.check_js_exec(target);
1202            }
1203            return Ok(());
1204        }
1205        Err(CapabilitiesError::JsExecDenied(path.to_owned()))
1206    }
1207}