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 pub memfd_type_override: Option<TypeId>,
19}
20
21impl ExceptionsConfig {
22 pub(super) fn new(policy: &Policy, exceptions: &[&str]) -> Result<Self, anyhow::Error> {
27 let mut result = Self {
28 todo_deny_entries: HashMap::with_capacity(exceptions.len()),
29 permissive_entries: HashMap::new(),
30 memfd_type_override: None,
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(&mut self, policy: &Policy, line: &str) -> Result<(), anyhow::Error> {
54 let mut parts = line.trim().split_whitespace();
55 if let Some(statement) = parts.next() {
56 match statement {
57 "todo_deny" => {
58 let bug_id = bug_ref_to_id(
63 parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
64 )?;
65
66 let stype = policy.type_id_by_name(
69 parts.next().ok_or_else(|| anyhow!("Expected source type"))?,
70 );
71 let ttype = policy.type_id_by_name(
72 parts.next().ok_or_else(|| anyhow!("Expected target type"))?,
73 );
74
75 let class_name = parts.next().ok_or_else(|| anyhow!("Target class missing"))?;
76
77 let policy_class = policy
81 .classes()
82 .iter()
83 .find(|x| x.class_name == class_name.as_bytes())
84 .map(|x| x.class_id);
85
86 let kernel_class = object_class_by_name(class_name);
89
90 if policy_class.is_none() && kernel_class.is_none() {
93 println!("Ignoring statement: {} (unknown class)", line);
94 return Ok(());
95 }
96
97 let (Some(source), Some(target)) = (stype, ttype) else {
100 println!("Ignoring statement: {} (unknown source or target)", line);
101 return Ok(());
102 };
103
104 if let Some(policy_class) = policy_class {
105 self.todo_deny_entries.insert(
106 ExceptionsEntry { source, target, class: policy_class.into() },
107 bug_id,
108 );
109 }
110 if let Some(kernel_class) = kernel_class {
111 self.todo_deny_entries.insert(
112 ExceptionsEntry { source, target, class: kernel_class.into() },
113 bug_id,
114 );
115 }
116 }
117 "todo_permissive" => {
118 let bug_id = bug_ref_to_id(
123 parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
124 )?;
125
126 let stype = policy.type_id_by_name(
128 parts.next().ok_or_else(|| anyhow!("Expected source type"))?,
129 );
130
131 if let Some(source) = stype {
132 self.permissive_entries.insert(source, bug_id);
133 } else {
134 println!("Ignoring statement: {}", line);
135 }
136 }
137 "memfd_type_override" => {
138 let memfd_type = policy.type_id_by_name(
141 parts.next().ok_or_else(|| anyhow!("Expected memfd type"))?,
142 );
143 if let Some(memfd_type) = memfd_type {
144 self.memfd_type_override = Some(memfd_type);
145 } else {
146 println!("Ignoring statement: {}", line);
147 }
148 }
149 _ => bail!("Unknown statement {}", statement),
150 }
151 }
152 Ok(())
153 }
154}
155
156#[derive(Eq, Hash, PartialEq)]
158struct ExceptionsEntry {
159 source: TypeId,
160 target: TypeId,
161 class: ObjectClass,
162}
163
164fn bug_ref_to_id(bug_ref: &str) -> Result<NonZeroU64, anyhow::Error> {
166 let bug_id_part = bug_ref
167 .strip_prefix("b/")
168 .or_else(|| bug_ref.strip_prefix("https://fxbug.dev/"))
169 .ok_or_else(|| {
170 anyhow!("Expected bug Identifier of the form b/<id> or https://fxbug.dev/<id>")
171 })?;
172 bug_id_part.parse::<NonZeroU64>().map_err(|_| anyhow!("Malformed bug Id: {}", bug_id_part))
173}
174
175fn object_class_by_name(name: &str) -> Option<KernelClass> {
178 KernelClass::all_variants().into_iter().find(|class| class.name() == name)
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::policy::parse_policy_by_value;
185 use std::sync::Arc;
186
187 const TEST_POLICY: &[u8] =
188 include_bytes!("../testdata/composite_policies/compiled/exceptions_config_policy.pp");
189
190 const EXCEPTION_SOURCE_TYPE: &str = "test_exception_source_t";
191 const EXCEPTION_TARGET_TYPE: &str = "test_exception_target_t";
192 const _EXCEPTION_OTHER_TYPE: &str = "test_exception_other_t";
193 const UNMATCHED_TYPE: &str = "test_exception_unmatched_t";
194
195 const NON_KERNEL_CLASS: &str = "test_exception_non_kernel_class";
196
197 const TEST_CONFIG: &[&str] = &[
198 "todo_deny b/001 test_exception_source_t test_exception_target_t file",
200 "todo_deny b/002 test_exception_other_t test_exception_target_t chr_file",
201 "todo_deny b/003 test_exception_source_t test_exception_other_t anon_inode",
204 "todo_deny b/004 test_exception_source_t test_exception_target_t test_exception_non_kernel_class",
207 "todo_deny b/101 test_undefined_source_t test_exception_target_t file",
209 "todo_deny b/102 test_exception_source_t test_undefined_target_t file",
210 "todo_deny b/103 test_exception_source_t test_exception_target_t test_exception_non_existent_class",
211 ];
212
213 struct TestData {
214 policy: Arc<Policy>,
215 defined_source: TypeId,
216 defined_target: TypeId,
217 unmatched_type: TypeId,
218 }
219
220 impl TestData {
221 fn expect_policy_class(&self, name: &str) -> ObjectClass {
222 self.policy
223 .classes()
224 .iter()
225 .find(|x| x.class_name == name.as_bytes())
226 .map(|x| x.class_id)
227 .expect("Unable to resolve policy class Id")
228 .into()
229 }
230 }
231 fn test_data() -> TestData {
232 let parsed = parse_policy_by_value(TEST_POLICY.to_vec()).unwrap();
233 let policy = Arc::new(parsed.validate().unwrap());
234 let defined_source = policy.type_id_by_name(EXCEPTION_SOURCE_TYPE).unwrap();
235 let defined_target = policy.type_id_by_name(EXCEPTION_TARGET_TYPE).unwrap();
236 let unmatched_type = policy.type_id_by_name(UNMATCHED_TYPE).unwrap();
237
238 assert!(policy.type_id_by_name("test_undefined_source_t").is_none());
239 assert!(policy.type_id_by_name("test_undefined_target_t").is_none());
240
241 TestData { policy, defined_source, defined_target, unmatched_type }
242 }
243
244 #[test]
245 fn empty_config_is_valid() {
246 let _ = ExceptionsConfig::new(&test_data().policy, &[])
247 .expect("Empty exceptions config is valid");
248 }
249
250 #[test]
251 fn extra_separating_whitespace_is_valid() {
252 let _ = ExceptionsConfig::new(
253 &test_data().policy,
254 &["
255 todo_deny b/001\ttest_exception_source_t test_exception_target_t file
256 "],
257 )
258 .expect("Config with extra separating whitespace is valid");
259 }
260
261 #[test]
262 fn only_defined_types_resolve_to_lookup_entries() {
263 let test_data = test_data();
264
265 let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
266 .expect("Config with unresolved types is valid");
267
268 assert_eq!(config.todo_deny_entries.len(), 6);
269 }
270
271 #[test]
272 fn lookup_matching() {
273 let test_data = test_data();
274
275 let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
276 .expect("Config with unresolved types is valid");
277
278 assert_eq!(
280 config.lookup(
281 test_data.defined_source,
282 test_data.defined_target,
283 KernelClass::File.into()
284 ),
285 Some(NonZeroU64::new(1).unwrap())
286 );
287
288 assert_eq!(
291 config.lookup(
292 test_data.defined_source,
293 test_data.defined_target,
294 test_data.expect_policy_class("file")
295 ),
296 Some(NonZeroU64::new(1).unwrap())
297 );
298
299 assert_eq!(
301 config.lookup(
302 test_data.defined_source,
303 test_data.defined_target,
304 test_data.expect_policy_class(NON_KERNEL_CLASS),
305 ),
306 Some(NonZeroU64::new(4).unwrap())
307 );
308
309 assert_eq!(
311 config.lookup(
312 test_data.defined_source,
313 test_data.defined_target,
314 KernelClass::Dir.into()
315 ),
316 None
317 );
318 assert_eq!(
319 config.lookup(
320 test_data.unmatched_type,
321 test_data.defined_target,
322 KernelClass::File.into()
323 ),
324 None
325 );
326 assert_eq!(
327 config.lookup(
328 test_data.defined_source,
329 test_data.unmatched_type,
330 KernelClass::File.into()
331 ),
332 None
333 );
334 }
335}