selinux/
exceptions_config.rs
1use crate::policy::parser::ByValue;
6use crate::policy::{Policy, TypeId};
7use crate::KernelClass;
8
9use anyhow::{anyhow, bail};
10use std::collections::HashMap;
11use std::num::NonZeroU64;
12
13pub(super) struct ExceptionsConfig {
15 todo_deny_entries: HashMap<ExceptionsEntry, NonZeroU64>,
16 permissive_entries: HashMap<TypeId, NonZeroU64>,
17}
18
19impl ExceptionsConfig {
20 pub(super) fn new(
25 policy: &Policy<ByValue<Vec<u8>>>,
26 exceptions_config: &str,
27 ) -> Result<Self, anyhow::Error> {
28 let lines = exceptions_config.lines();
29 let mut result = Self {
30 todo_deny_entries: HashMap::with_capacity(lines.clone().count()),
31 permissive_entries: HashMap::new(),
32 };
33 for line in lines {
34 result.parse_config_line(policy, line)?;
35 }
36 result.todo_deny_entries.shrink_to_fit();
37 Ok(result)
38 }
39
40 pub(super) fn lookup(
43 &self,
44 source: TypeId,
45 target: TypeId,
46 class: KernelClass,
47 ) -> Option<NonZeroU64> {
48 self.todo_deny_entries
49 .get(&ExceptionsEntry { source, target, class })
50 .or_else(|| self.permissive_entries.get(&source))
51 .copied()
52 }
53
54 fn parse_config_line(
55 &mut self,
56 policy: &Policy<ByValue<Vec<u8>>>,
57 line: &str,
58 ) -> Result<(), anyhow::Error> {
59 let mut parts = line.trim().split_whitespace();
60 if let Some(statement) = parts.next() {
61 match statement {
62 "todo_deny" => {
63 let bug_id = bug_ref_to_id(
68 parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
69 )?;
70
71 let stype = policy.type_id_by_name(
74 parts.next().ok_or_else(|| anyhow!("Expected source type"))?,
75 );
76 let ttype = policy.type_id_by_name(
77 parts.next().ok_or_else(|| anyhow!("Expected target type"))?,
78 );
79
80 let class = parts
83 .next()
84 .and_then(object_class_by_name)
85 .ok_or_else(|| anyhow!("Target class missing or unrecognized"))?;
86
87 if let (Some(source), Some(target)) = (stype, ttype) {
88 self.todo_deny_entries
89 .insert(ExceptionsEntry { source, target, class }, bug_id);
90 } else {
91 println!("Ignoring statement: {}", line);
92 }
93 }
94 "todo_permissive" => {
95 let bug_id = bug_ref_to_id(
100 parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
101 )?;
102
103 let stype = policy.type_id_by_name(
105 parts.next().ok_or_else(|| anyhow!("Expected source type"))?,
106 );
107
108 if let Some(source) = stype {
109 self.permissive_entries.insert(source, bug_id);
110 } else {
111 println!("Ignoring statement: {}", line);
112 }
113 }
114 "" | "//" => {}
115 _ => bail!("Unknown statement {}", statement),
116 }
117 }
118 Ok(())
119 }
120}
121
122#[derive(Eq, Hash, PartialEq)]
124struct ExceptionsEntry {
125 source: TypeId,
126 target: TypeId,
127 class: KernelClass,
128}
129
130fn bug_ref_to_id(bug_ref: &str) -> Result<NonZeroU64, anyhow::Error> {
132 let bug_id_part = bug_ref
133 .strip_prefix("b/")
134 .or_else(|| bug_ref.strip_prefix("https://fxbug.dev/"))
135 .ok_or_else(|| {
136 anyhow!("Expected bug Identifier of the form b/<id> or https://fxbug.dev/<id>")
137 })?;
138 bug_id_part.parse::<NonZeroU64>().map_err(|_| anyhow!("Malformed bug Id: {}", bug_id_part))
139}
140
141fn object_class_by_name(name: &str) -> Option<KernelClass> {
144 KernelClass::all_variants().into_iter().find(|class| class.name() == name)
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::policy::parse_policy_by_value;
151 use std::sync::Arc;
152
153 const TEST_POLICY: &[u8] =
154 include_bytes!("../testdata/composite_policies/compiled/exceptions_config_policy.pp");
155
156 const EXCEPTION_SOURCE_TYPE: &str = "test_exception_source_t";
157 const EXCEPTION_TARGET_TYPE: &str = "test_exception_target_t";
158 const _EXCEPTION_OTHER_TYPE: &str = "test_exception_other_t";
159 const UNMATCHED_TYPE: &str = "test_exception_unmatched_t";
160
161 struct TestData {
162 policy: Arc<Policy<ByValue<Vec<u8>>>>,
163 defined_source: TypeId,
164 defined_target: TypeId,
165 unmatched_type: TypeId,
166 }
167
168 fn test_data() -> TestData {
169 let (parsed, _) = parse_policy_by_value(TEST_POLICY.to_vec()).unwrap();
170 let policy = Arc::new(parsed.validate().unwrap());
171 let defined_source = policy.type_id_by_name(EXCEPTION_SOURCE_TYPE).unwrap();
172 let defined_target = policy.type_id_by_name(EXCEPTION_TARGET_TYPE).unwrap();
173 let unmatched_type = policy.type_id_by_name(UNMATCHED_TYPE).unwrap();
174
175 assert!(policy.type_id_by_name("test_undefined_source_t").is_none());
176 assert!(policy.type_id_by_name("test_undefined_target_t").is_none());
177
178 TestData { policy, defined_source, defined_target, unmatched_type }
179 }
180
181 #[test]
182 fn empty_config_is_valid() {
183 let _ = ExceptionsConfig::new(&test_data().policy, "")
184 .expect("Empty exceptions config is valid");
185 }
186
187 #[test]
188 fn comments_and_empty_lines_are_valid() {
189 let _ = ExceptionsConfig::new(
190 &test_data().policy,
191 "
192 // This is a comment.
193
194 // This is a second comment, with a blank line preceding it.
195 ",
196 )
197 .expect("Config with only comments is valid");
198 }
199
200 #[test]
201 fn extra_separating_whitespace_is_valid() {
202 let _ = ExceptionsConfig::new(
203 &test_data().policy,
204 "
205 todo_deny b/001\ttest_exception_source_t test_exception_target_t file
206 ",
207 )
208 .expect("Config with extra separating whitespace is valid");
209 }
210
211 const TEST_CONFIG: &str = "
212 // These statement should all be resolved.
213 todo_deny b/001 test_exception_source_t test_exception_target_t file
214 todo_deny b/002 test_exception_other_t test_exception_target_t chr_file
215 todo_deny b/003 test_exception_source_t test_exception_other_t anon_inode
216
217 // These statements should not be resolved.
218 todo_deny b/101 test_undefined_source_t test_exception_target_t file
219 todo_deny b/102 test_exception_source_t test_undefined_target_t file
220 ";
221
222 #[test]
223 fn only_defined_types_resolve_to_lookup_entries() {
224 let test_data = test_data();
225
226 let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
227 .expect("Config with unresolved types is valid");
228
229 assert_eq!(config.todo_deny_entries.len(), 3);
230 }
231
232 #[test]
233 fn lookup_matching() {
234 let test_data = test_data();
235
236 let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
237 .expect("Config with unresolved types is valid");
238
239 assert_eq!(
241 config.lookup(test_data.defined_source, test_data.defined_target, KernelClass::File),
242 Some(NonZeroU64::new(1).unwrap())
243 );
244
245 assert_eq!(
247 config.lookup(test_data.defined_source, test_data.defined_target, KernelClass::Dir),
248 None
249 );
250 assert_eq!(
251 config.lookup(test_data.unmatched_type, test_data.defined_target, KernelClass::File),
252 None
253 );
254 assert_eq!(
255 config.lookup(test_data.defined_source, test_data.unmatched_type, KernelClass::File),
256 None
257 );
258 }
259}