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