1use crate::new_policy::traits::HasPolicyId;
6use crate::policy::{Policy, TypeId};
7use crate::{KernelClass, ObjectClass};
8
9use anyhow::{anyhow, bail};
10use std::collections::HashMap;
11use std::num::NonZeroU32;
12use strum::VariantArray as _;
13
14pub(super) struct ExceptionsConfig {
16 todo_deny_entries: HashMap<ExceptionsEntry, NonZeroU32>,
17 permissive_entries: HashMap<TypeId, NonZeroU32>,
18}
19
20impl ExceptionsConfig {
21 pub(super) fn new(policy: &Policy, exceptions: &[&str]) -> Result<Self, anyhow::Error> {
26 let mut result = Self {
27 todo_deny_entries: HashMap::with_capacity(exceptions.len()),
28 permissive_entries: HashMap::new(),
29 };
30 for line in exceptions {
31 result.parse_config_line(policy, line)?;
32 }
33 result.todo_deny_entries.shrink_to_fit();
34 Ok(result)
35 }
36
37 pub(super) fn lookup(
40 &self,
41 source: TypeId,
42 target: TypeId,
43 class: ObjectClass,
44 ) -> Option<NonZeroU32> {
45 self.todo_deny_entries
46 .get(&ExceptionsEntry { source, target, class })
47 .or_else(|| self.permissive_entries.get(&source))
48 .copied()
49 }
50
51 fn parse_config_line(&mut self, policy: &Policy, line: &str) -> Result<(), anyhow::Error> {
52 let mut parts = line.trim().split_whitespace();
53 if let Some(statement) = parts.next() {
54 match statement {
55 "todo_deny" => {
56 let bug_id = bug_ref_to_id(
61 parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
62 )?;
63
64 let stype = policy
67 .types()
68 .get_by_name(
69 parts.next().ok_or_else(|| anyhow!("Expected source type"))?.as_bytes(),
70 )
71 .map(|t| t.id());
72 let ttype = policy
73 .types()
74 .get_by_name(
75 parts.next().ok_or_else(|| anyhow!("Expected target type"))?.as_bytes(),
76 )
77 .map(|t| t.id());
78
79 let class_name = parts.next().ok_or_else(|| anyhow!("Target class missing"))?;
80
81 let policy_class =
85 policy.classes().get_by_name(class_name.as_bytes()).map(|x| x.id());
86
87 let kernel_class = object_class_by_name(class_name);
90
91 if policy_class.is_none() && kernel_class.is_none() {
94 println!("Ignoring statement: {} (unknown class)", line);
95 return Ok(());
96 }
97
98 let (Some(source), Some(target)) = (stype, ttype) else {
101 println!("Ignoring statement: {} (unknown source or target)", line);
102 return Ok(());
103 };
104
105 if let Some(policy_class) = policy_class {
106 self.todo_deny_entries.insert(
107 ExceptionsEntry { source, target, class: policy_class.into() },
108 bug_id,
109 );
110 }
111 if let Some(kernel_class) = kernel_class {
112 self.todo_deny_entries.insert(
113 ExceptionsEntry { source, target, class: kernel_class.into() },
114 bug_id,
115 );
116 }
117 }
118 "todo_permissive" => {
119 let bug_id = bug_ref_to_id(
124 parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
125 )?;
126
127 let stype = policy
129 .types()
130 .get_by_name(
131 parts.next().ok_or_else(|| anyhow!("Expected source type"))?.as_bytes(),
132 )
133 .map(|t| t.id());
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<NonZeroU32, 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::<NonZeroU32>().map_err(|_| anyhow!("Malformed bug Id: {}", bug_id_part))
165}
166
167fn object_class_by_name(name: &str) -> Option<KernelClass> {
170 KernelClass::VARIANTS.iter().find(|class| class.name() == name).map(Clone::clone)
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");
181
182 const EXCEPTION_SOURCE_TYPE: &[u8] = b"test_exception_source_t";
183 const EXCEPTION_TARGET_TYPE: &[u8] = b"test_exception_target_t";
184 const _EXCEPTION_OTHER_TYPE: &[u8] = b"test_exception_other_t";
185 const UNMATCHED_TYPE: &[u8] = b"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>,
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 .get_by_name(name.as_bytes())
217 .map(|x| x.id())
218 .expect("Unable to resolve policy class Id")
219 .into()
220 }
221 }
222 fn test_data() -> TestData {
223 let parsed = parse_policy_by_value(TEST_POLICY.to_vec()).unwrap();
224 let policy = Arc::new(parsed.validate().unwrap());
225 let defined_source = policy.types().get_by_name(EXCEPTION_SOURCE_TYPE).unwrap().id();
226 let defined_target = policy.types().get_by_name(EXCEPTION_TARGET_TYPE).unwrap().id();
227 let unmatched_type = policy.types().get_by_name(UNMATCHED_TYPE).unwrap().id();
228
229 assert!(policy.types().get_by_name(b"test_undefined_source_t").is_none());
230 assert!(policy.types().get_by_name(b"test_undefined_target_t").is_none());
231
232 TestData { policy, defined_source, defined_target, unmatched_type }
233 }
234
235 #[test]
236 fn empty_config_is_valid() {
237 let _ = ExceptionsConfig::new(&test_data().policy, &[])
238 .expect("Empty exceptions config is valid");
239 }
240
241 #[test]
242 fn extra_separating_whitespace_is_valid() {
243 let _ = ExceptionsConfig::new(
244 &test_data().policy,
245 &["
246 todo_deny b/001\ttest_exception_source_t test_exception_target_t file
247 "],
248 )
249 .expect("Config with extra separating whitespace is valid");
250 }
251
252 #[test]
253 fn only_defined_types_resolve_to_lookup_entries() {
254 let test_data = test_data();
255
256 let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
257 .expect("Config with unresolved types is valid");
258
259 assert_eq!(config.todo_deny_entries.len(), 6);
260 }
261
262 #[test]
263 fn lookup_matching() {
264 let test_data = test_data();
265
266 let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
267 .expect("Config with unresolved types is valid");
268
269 assert_eq!(
271 config.lookup(
272 test_data.defined_source,
273 test_data.defined_target,
274 KernelClass::File.into()
275 ),
276 Some(NonZeroU32::new(1).unwrap())
277 );
278
279 assert_eq!(
282 config.lookup(
283 test_data.defined_source,
284 test_data.defined_target,
285 test_data.expect_policy_class("file")
286 ),
287 Some(NonZeroU32::new(1).unwrap())
288 );
289
290 assert_eq!(
292 config.lookup(
293 test_data.defined_source,
294 test_data.defined_target,
295 test_data.expect_policy_class(NON_KERNEL_CLASS),
296 ),
297 Some(NonZeroU32::new(4).unwrap())
298 );
299
300 assert_eq!(
302 config.lookup(
303 test_data.defined_source,
304 test_data.defined_target,
305 KernelClass::Dir.into()
306 ),
307 None
308 );
309 assert_eq!(
310 config.lookup(
311 test_data.unmatched_type,
312 test_data.defined_target,
313 KernelClass::File.into()
314 ),
315 None
316 );
317 assert_eq!(
318 config.lookup(
319 test_data.defined_source,
320 test_data.unmatched_type,
321 KernelClass::File.into()
322 ),
323 None
324 );
325 }
326}