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