Skip to main content

boundgraph_grant/
access.rs

1//! Read or write: which side of the disjointness condition an action falls on.
2//!
3//! `par` may run two branches concurrently only if they cannot interfere. Two
4//! branches reading the same file interfere with nothing; one writing while the
5//! other reads is a race. Deciding that needs each action classified.
6//!
7//! # Conservative by default
8//!
9//! An unrecognised action is classified as a **write**. That is Goal G5 applied
10//! here: misclassifying a write as a read admits a real data race, while
11//! misclassifying a read as a write costs only some lost parallelism. The
12//! failure modes are not symmetric, so the default is not neutral.
13
14/// Whether an action observes a resource or changes it.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum Access {
17    /// Observes without changing. Two reads never conflict.
18    Read,
19    /// May change the resource, or have an effect beyond it.
20    Write,
21}
22
23impl Access {
24    /// Whether this is a write.
25    pub fn is_write(self) -> bool {
26        self == Self::Write
27    }
28}
29
30/// Actions known to observe without changing anything.
31const READ_ACTIONS: &[&str] = &["read", "list", "stat", "now", "bytes", "use"];
32
33/// Actions known to change something, or to have an effect beyond the process.
34const WRITE_ACTIONS: &[&str] = &[
35    "write", "append", "create", "delete", "truncate", "exec", "egress",
36];
37
38/// Classify an action.
39///
40/// Note two calls that may look surprising, both deliberate:
41///
42/// - `exec` is a **write**. Running a process can do anything the process can.
43/// - `egress` is a **write**. Sending a request may change remote state, so two
44///   branches talking to the same host are not provably independent.
45///
46/// And `use`, as in `secret:use:stripe_key`, is a **read**: two branches using
47/// the same credential do not interfere with each other.
48pub fn classify(action: &str) -> Access {
49    if READ_ACTIONS.contains(&action) {
50        Access::Read
51    } else if WRITE_ACTIONS.contains(&action) {
52        Access::Write
53    } else {
54        // Unrecognised: assume the more dangerous reading.
55        Access::Write
56    }
57}
58
59/// Whether the action is one this crate recognises.
60///
61/// An unrecognised action still classifies (as a write); this only says whether
62/// the classification came from knowledge or from the conservative default,
63/// which is worth surfacing in a diagnostic.
64pub fn is_known_action(action: &str) -> bool {
65    READ_ACTIONS.contains(&action) || WRITE_ACTIONS.contains(&action)
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn observing_actions_are_reads() {
74        for a in ["read", "list", "stat", "now", "bytes", "use"] {
75            assert_eq!(classify(a), Access::Read, "{a} should be a read");
76        }
77    }
78
79    #[test]
80    fn mutating_actions_are_writes() {
81        for a in ["write", "append", "create", "delete", "exec", "egress"] {
82            assert_eq!(classify(a), Access::Write, "{a} should be a write");
83        }
84    }
85
86    #[test]
87    fn an_unknown_action_is_assumed_to_write() {
88        assert_eq!(classify("frobnicate"), Access::Write);
89        assert!(!is_known_action("frobnicate"));
90        assert!(is_known_action("read"));
91    }
92
93    #[test]
94    fn executing_a_process_counts_as_writing() {
95        // A subprocess can do anything its capabilities allow, so two branches
96        // that both exec are not provably independent.
97        assert_eq!(classify("exec"), Access::Write);
98    }
99
100    #[test]
101    fn network_egress_counts_as_writing() {
102        assert_eq!(classify("egress"), Access::Write);
103    }
104
105    #[test]
106    fn using_a_secret_counts_as_reading() {
107        assert_eq!(classify("use"), Access::Read);
108    }
109}