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}