1use crate::policy::{HasPolicyId as _, Policy, TypeId};
6use crate::{KernelClass, ObjectClass};
7
8use anyhow::{anyhow, bail};
9use std::collections::HashMap;
10use std::num::NonZeroU32;
11use strum::VariantArray as _;
12
13pub(super) struct ExceptionsConfig {
15 todo_deny_entries: HashMap<ExceptionsEntry, NonZeroU32>,
16 permissive_entries: HashMap<TypeId, NonZeroU32>,
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<NonZeroU32> {
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
66 .types()
67 .get_by_name(
68 parts.next().ok_or_else(|| anyhow!("Expected source type"))?.as_bytes(),
69 )
70 .map(|t| t.id());
71 let ttype = policy
72 .types()
73 .get_by_name(
74 parts.next().ok_or_else(|| anyhow!("Expected target type"))?.as_bytes(),
75 )
76 .map(|t| t.id());
77
78 let class_name = parts.next().ok_or_else(|| anyhow!("Target class missing"))?;
79
80 let policy_class =
84 policy.classes().get_by_name(class_name.as_bytes()).map(|x| x.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
128 .types()
129 .get_by_name(
130 parts.next().ok_or_else(|| anyhow!("Expected source type"))?.as_bytes(),
131 )
132 .map(|t| t.id());
133
134 if let Some(source) = stype {
135 self.permissive_entries.insert(source, bug_id);
136 } else {
137 println!("Ignoring statement: {}", line);
138 }
139 }
140 _ => bail!("Unknown statement {}", statement),
141 }
142 }
143 Ok(())
144 }
145}
146
147#[derive(Eq, Hash, PartialEq)]
149struct ExceptionsEntry {
150 source: TypeId,
151 target: TypeId,
152 class: ObjectClass,
153}
154
155fn bug_ref_to_id(bug_ref: &str) -> Result<NonZeroU32, anyhow::Error> {
157 let bug_id_part = bug_ref
158 .strip_prefix("b/")
159 .or_else(|| bug_ref.strip_prefix("https://fxbug.dev/"))
160 .ok_or_else(|| {
161 anyhow!("Expected bug Identifier of the form b/<id> or https://fxbug.dev/<id>")
162 })?;
163 bug_id_part.parse::<NonZeroU32>().map_err(|_| anyhow!("Malformed bug Id: {}", bug_id_part))
164}
165
166fn object_class_by_name(name: &str) -> Option<KernelClass> {
169 KernelClass::VARIANTS.iter().find(|class| class.name() == name).map(Clone::clone)
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 const TEST_POLICY: &[u8] =
177 include_bytes!("../testdata/composite_policies/compiled/exceptions_config_policy");
178
179 const EXCEPTION_SOURCE_TYPE: &[u8] = b"test_exception_source_t";
180 const EXCEPTION_TARGET_TYPE: &[u8] = b"test_exception_target_t";
181 const _EXCEPTION_OTHER_TYPE: &[u8] = b"test_exception_other_t";
182 const UNMATCHED_TYPE: &[u8] = b"test_exception_unmatched_t";
183
184 const NON_KERNEL_CLASS: &str = "test_exception_non_kernel_class";
185
186 const TEST_CONFIG: &[&str] = &[
187 "todo_deny b/001 test_exception_source_t test_exception_target_t file",
189 "todo_deny b/002 test_exception_other_t test_exception_target_t chr_file",
190 "todo_deny b/003 test_exception_source_t test_exception_other_t anon_inode",
193 "todo_deny b/004 test_exception_source_t test_exception_target_t test_exception_non_kernel_class",
196 "todo_deny b/101 test_undefined_source_t test_exception_target_t file",
198 "todo_deny b/102 test_exception_source_t test_undefined_target_t file",
199 "todo_deny b/103 test_exception_source_t test_exception_target_t test_exception_non_existent_class",
200 ];
201
202 struct TestData {
203 policy: Policy,
204 defined_source: TypeId,
205 defined_target: TypeId,
206 unmatched_type: TypeId,
207 }
208
209 impl TestData {
210 fn expect_policy_class(&self, name: &str) -> ObjectClass {
211 self.policy
212 .classes()
213 .get_by_name(name.as_bytes())
214 .map(|x| x.id())
215 .expect("Unable to resolve policy class Id")
216 .into()
217 }
218 }
219 fn test_data() -> TestData {
220 let policy = Policy::parse(TEST_POLICY).unwrap();
221 policy.validate().unwrap();
222 let defined_source = policy.types().get_by_name(EXCEPTION_SOURCE_TYPE).unwrap().id();
223 let defined_target = policy.types().get_by_name(EXCEPTION_TARGET_TYPE).unwrap().id();
224 let unmatched_type = policy.types().get_by_name(UNMATCHED_TYPE).unwrap().id();
225
226 assert!(policy.types().get_by_name(b"test_undefined_source_t").is_none());
227 assert!(policy.types().get_by_name(b"test_undefined_target_t").is_none());
228
229 TestData { policy, defined_source, defined_target, unmatched_type }
230 }
231
232 #[test]
233 fn empty_config_is_valid() {
234 let _ = ExceptionsConfig::new(&test_data().policy, &[])
235 .expect("Empty exceptions config is valid");
236 }
237
238 #[test]
239 fn extra_separating_whitespace_is_valid() {
240 let _ = ExceptionsConfig::new(
241 &test_data().policy,
242 &["
243 todo_deny b/001\ttest_exception_source_t test_exception_target_t file
244 "],
245 )
246 .expect("Config with extra separating whitespace is valid");
247 }
248
249 #[test]
250 fn only_defined_types_resolve_to_lookup_entries() {
251 let test_data = test_data();
252
253 let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
254 .expect("Config with unresolved types is valid");
255
256 assert_eq!(config.todo_deny_entries.len(), 6);
257 }
258
259 #[test]
260 fn lookup_matching() {
261 let test_data = test_data();
262
263 let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
264 .expect("Config with unresolved types is valid");
265
266 assert_eq!(
268 config.lookup(
269 test_data.defined_source,
270 test_data.defined_target,
271 KernelClass::File.into()
272 ),
273 Some(NonZeroU32::new(1).unwrap())
274 );
275
276 assert_eq!(
279 config.lookup(
280 test_data.defined_source,
281 test_data.defined_target,
282 test_data.expect_policy_class("file")
283 ),
284 Some(NonZeroU32::new(1).unwrap())
285 );
286
287 assert_eq!(
289 config.lookup(
290 test_data.defined_source,
291 test_data.defined_target,
292 test_data.expect_policy_class(NON_KERNEL_CLASS),
293 ),
294 Some(NonZeroU32::new(4).unwrap())
295 );
296
297 assert_eq!(
299 config.lookup(
300 test_data.defined_source,
301 test_data.defined_target,
302 KernelClass::Dir.into()
303 ),
304 None
305 );
306 assert_eq!(
307 config.lookup(
308 test_data.unmatched_type,
309 test_data.defined_target,
310 KernelClass::File.into()
311 ),
312 None
313 );
314 assert_eq!(
315 config.lookup(
316 test_data.defined_source,
317 test_data.unmatched_type,
318 KernelClass::File.into()
319 ),
320 None
321 );
322 }
323}