1use crate::policy::parser::ByValue;
6use crate::policy::{Policy, TypeId};
7use crate::{KernelClass, ObjectClass};
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: &[&str],
27 ) -> Result<Self, anyhow::Error> {
28 let mut result = Self {
29 todo_deny_entries: HashMap::with_capacity(exceptions.len()),
30 permissive_entries: HashMap::new(),
31 };
32 for line in exceptions {
33 result.parse_config_line(policy, line)?;
34 }
35 result.todo_deny_entries.shrink_to_fit();
36 Ok(result)
37 }
38
39 pub(super) fn lookup(
42 &self,
43 source: TypeId,
44 target: TypeId,
45 class: ObjectClass,
46 ) -> Option<NonZeroU64> {
47 self.todo_deny_entries
48 .get(&ExceptionsEntry { source, target, class })
49 .or_else(|| self.permissive_entries.get(&source))
50 .copied()
51 }
52
53 fn parse_config_line(
54 &mut self,
55 policy: &Policy<ByValue<Vec<u8>>>,
56 line: &str,
57 ) -> Result<(), anyhow::Error> {
58 let mut parts = line.trim().split_whitespace();
59 if let Some(statement) = parts.next() {
60 match statement {
61 "todo_deny" => {
62 let bug_id = bug_ref_to_id(
67 parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
68 )?;
69
70 let stype = policy.type_id_by_name(
73 parts.next().ok_or_else(|| anyhow!("Expected source type"))?,
74 );
75 let ttype = policy.type_id_by_name(
76 parts.next().ok_or_else(|| anyhow!("Expected target type"))?,
77 );
78
79 let class_name = parts.next().ok_or_else(|| anyhow!("Target class missing"))?;
80
81 let policy_class = policy
85 .classes()
86 .iter()
87 .find(|x| x.class_name == class_name.as_bytes())
88 .map(|x| x.class_id);
89
90 let kernel_class = object_class_by_name(class_name);
93
94 if policy_class.is_none() && kernel_class.is_none() {
97 println!("Ignoring statement: {} (unknown class)", line);
98 return Ok(());
99 }
100
101 let (Some(source), Some(target)) = (stype, ttype) else {
104 println!("Ignoring statement: {} (unknown source or target)", line);
105 return Ok(());
106 };
107
108 if let Some(policy_class) = policy_class {
109 self.todo_deny_entries.insert(
110 ExceptionsEntry { source, target, class: policy_class.into() },
111 bug_id,
112 );
113 }
114 if let Some(kernel_class) = kernel_class {
115 self.todo_deny_entries.insert(
116 ExceptionsEntry { source, target, class: kernel_class.into() },
117 bug_id,
118 );
119 }
120 }
121 "todo_permissive" => {
122 let bug_id = bug_ref_to_id(
127 parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
128 )?;
129
130 let stype = policy.type_id_by_name(
132 parts.next().ok_or_else(|| anyhow!("Expected source type"))?,
133 );
134
135 if let Some(source) = stype {
136 self.permissive_entries.insert(source, bug_id);
137 } else {
138 println!("Ignoring statement: {}", line);
139 }
140 }
141 _ => bail!("Unknown statement {}", statement),
142 }
143 }
144 Ok(())
145 }
146}
147
148#[derive(Eq, Hash, PartialEq)]
150struct ExceptionsEntry {
151 source: TypeId,
152 target: TypeId,
153 class: ObjectClass,
154}
155
156fn bug_ref_to_id(bug_ref: &str) -> Result<NonZeroU64, anyhow::Error> {
158 let bug_id_part = bug_ref
159 .strip_prefix("b/")
160 .or_else(|| bug_ref.strip_prefix("https://fxbug.dev/"))
161 .ok_or_else(|| {
162 anyhow!("Expected bug Identifier of the form b/<id> or https://fxbug.dev/<id>")
163 })?;
164 bug_id_part.parse::<NonZeroU64>().map_err(|_| anyhow!("Malformed bug Id: {}", bug_id_part))
165}
166
167fn object_class_by_name(name: &str) -> Option<KernelClass> {
170 KernelClass::all_variants().into_iter().find(|class| class.name() == name)
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::policy::parse_policy_by_value;
177 use std::sync::Arc;
178
179 const TEST_POLICY: &[u8] =
180 include_bytes!("../testdata/composite_policies/compiled/exceptions_config_policy.pp");
181
182 const EXCEPTION_SOURCE_TYPE: &str = "test_exception_source_t";
183 const EXCEPTION_TARGET_TYPE: &str = "test_exception_target_t";
184 const _EXCEPTION_OTHER_TYPE: &str = "test_exception_other_t";
185 const UNMATCHED_TYPE: &str = "test_exception_unmatched_t";
186
187 const NON_KERNEL_CLASS: &str = "test_exception_non_kernel_class";
188
189 const TEST_CONFIG: &[&str] = &[
190 "todo_deny b/001 test_exception_source_t test_exception_target_t file",
192 "todo_deny b/002 test_exception_other_t test_exception_target_t chr_file",
193 "todo_deny b/003 test_exception_source_t test_exception_other_t anon_inode",
196 "todo_deny b/004 test_exception_source_t test_exception_target_t test_exception_non_kernel_class",
199 "todo_deny b/101 test_undefined_source_t test_exception_target_t file",
201 "todo_deny b/102 test_exception_source_t test_undefined_target_t file",
202 "todo_deny b/103 test_exception_source_t test_exception_target_t test_exception_non_existent_class",
203 ];
204
205 struct TestData {
206 policy: Arc<Policy<ByValue<Vec<u8>>>>,
207 defined_source: TypeId,
208 defined_target: TypeId,
209 unmatched_type: TypeId,
210 }
211
212 impl TestData {
213 fn expect_policy_class(&self, name: &str) -> ObjectClass {
214 self.policy
215 .classes()
216 .iter()
217 .find(|x| x.class_name == name.as_bytes())
218 .map(|x| x.class_id)
219 .expect("Unable to resolve policy class Id")
220 .into()
221 }
222 }
223 fn test_data() -> TestData {
224 let (parsed, _) = parse_policy_by_value(TEST_POLICY.to_vec()).unwrap();
225 let policy = Arc::new(parsed.validate().unwrap());
226 let defined_source = policy.type_id_by_name(EXCEPTION_SOURCE_TYPE).unwrap();
227 let defined_target = policy.type_id_by_name(EXCEPTION_TARGET_TYPE).unwrap();
228 let unmatched_type = policy.type_id_by_name(UNMATCHED_TYPE).unwrap();
229
230 assert!(policy.type_id_by_name("test_undefined_source_t").is_none());
231 assert!(policy.type_id_by_name("test_undefined_target_t").is_none());
232
233 TestData { policy, defined_source, defined_target, unmatched_type }
234 }
235
236 #[test]
237 fn empty_config_is_valid() {
238 let _ = ExceptionsConfig::new(&test_data().policy, &[])
239 .expect("Empty exceptions config is valid");
240 }
241
242 #[test]
243 fn extra_separating_whitespace_is_valid() {
244 let _ = ExceptionsConfig::new(
245 &test_data().policy,
246 &["
247 todo_deny b/001\ttest_exception_source_t test_exception_target_t file
248 "],
249 )
250 .expect("Config with extra separating whitespace is valid");
251 }
252
253 #[test]
254 fn only_defined_types_resolve_to_lookup_entries() {
255 let test_data = test_data();
256
257 let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
258 .expect("Config with unresolved types is valid");
259
260 assert_eq!(config.todo_deny_entries.len(), 6);
261 }
262
263 #[test]
264 fn lookup_matching() {
265 let test_data = test_data();
266
267 let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
268 .expect("Config with unresolved types is valid");
269
270 assert_eq!(
272 config.lookup(
273 test_data.defined_source,
274 test_data.defined_target,
275 KernelClass::File.into()
276 ),
277 Some(NonZeroU64::new(1).unwrap())
278 );
279
280 assert_eq!(
283 config.lookup(
284 test_data.defined_source,
285 test_data.defined_target,
286 test_data.expect_policy_class("file")
287 ),
288 Some(NonZeroU64::new(1).unwrap())
289 );
290
291 assert_eq!(
293 config.lookup(
294 test_data.defined_source,
295 test_data.defined_target,
296 test_data.expect_policy_class(NON_KERNEL_CLASS),
297 ),
298 Some(NonZeroU64::new(4).unwrap())
299 );
300
301 assert_eq!(
303 config.lookup(
304 test_data.defined_source,
305 test_data.defined_target,
306 KernelClass::Dir.into()
307 ),
308 None
309 );
310 assert_eq!(
311 config.lookup(
312 test_data.unmatched_type,
313 test_data.defined_target,
314 KernelClass::File.into()
315 ),
316 None
317 );
318 assert_eq!(
319 config.lookup(
320 test_data.defined_source,
321 test_data.unmatched_type,
322 KernelClass::File.into()
323 ),
324 None
325 );
326 }
327}