selinux/
exceptions_config.rs1use crate::policy::parser::ByValue;
6use crate::policy::{Policy, TypeId};
7use crate::KernelClass;
8
9use anyhow::{anyhow, bail};
10use std::collections::HashMap;
11use std::num::NonZeroU64;
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(
25 policy: &Policy<ByValue<Vec<u8>>>,
26 exceptions: &[&str],
27 ) -> Result<Self, anyhow::Error> {
28 let mut result = Self {
29 todo_deny_entries: HashMap::with_capacity(exceptions.len()),
30 permissive_entries: HashMap::new(),
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: KernelClass,
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(
54 &mut self,
55 policy: &Policy<ByValue<Vec<u8>>>,
56 line: &str,
57 ) -> Result<(), anyhow::Error> {
58 let mut parts = line.trim().split_whitespace();
59 if let Some(statement) = parts.next() {
60 match statement {
61 "todo_deny" => {
62 let bug_id = bug_ref_to_id(
67 parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
68 )?;
69
70 let stype = policy.type_id_by_name(
73 parts.next().ok_or_else(|| anyhow!("Expected source type"))?,
74 );
75 let ttype = policy.type_id_by_name(
76 parts.next().ok_or_else(|| anyhow!("Expected target type"))?,
77 );
78
79 let class = parts
82 .next()
83 .and_then(object_class_by_name)
84 .ok_or_else(|| anyhow!("Target class missing or unrecognized"))?;
85
86 if let (Some(source), Some(target)) = (stype, ttype) {
87 self.todo_deny_entries
88 .insert(ExceptionsEntry { source, target, class }, bug_id);
89 } else {
90 println!("Ignoring statement: {}", line);
91 }
92 }
93 "todo_permissive" => {
94 let bug_id = bug_ref_to_id(
99 parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
100 )?;
101
102 let stype = policy.type_id_by_name(
104 parts.next().ok_or_else(|| anyhow!("Expected source type"))?,
105 );
106
107 if let Some(source) = stype {
108 self.permissive_entries.insert(source, bug_id);
109 } else {
110 println!("Ignoring statement: {}", line);
111 }
112 }
113 _ => bail!("Unknown statement {}", statement),
114 }
115 }
116 Ok(())
117 }
118}
119
120#[derive(Eq, Hash, PartialEq)]
122struct ExceptionsEntry {
123 source: TypeId,
124 target: TypeId,
125 class: KernelClass,
126}
127
128fn bug_ref_to_id(bug_ref: &str) -> Result<NonZeroU64, anyhow::Error> {
130 let bug_id_part = bug_ref
131 .strip_prefix("b/")
132 .or_else(|| bug_ref.strip_prefix("https://fxbug.dev/"))
133 .ok_or_else(|| {
134 anyhow!("Expected bug Identifier of the form b/<id> or https://fxbug.dev/<id>")
135 })?;
136 bug_id_part.parse::<NonZeroU64>().map_err(|_| anyhow!("Malformed bug Id: {}", bug_id_part))
137}
138
139fn object_class_by_name(name: &str) -> Option<KernelClass> {
142 KernelClass::all_variants().into_iter().find(|class| class.name() == name)
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::policy::parse_policy_by_value;
149 use std::sync::Arc;
150
151 const TEST_POLICY: &[u8] =
152 include_bytes!("../testdata/composite_policies/compiled/exceptions_config_policy.pp");
153
154 const EXCEPTION_SOURCE_TYPE: &str = "test_exception_source_t";
155 const EXCEPTION_TARGET_TYPE: &str = "test_exception_target_t";
156 const _EXCEPTION_OTHER_TYPE: &str = "test_exception_other_t";
157 const UNMATCHED_TYPE: &str = "test_exception_unmatched_t";
158
159 const TEST_CONFIG: &[&str] = &[
160 "todo_deny b/001 test_exception_source_t test_exception_target_t file",
162 "todo_deny b/002 test_exception_other_t test_exception_target_t chr_file",
163 "todo_deny b/003 test_exception_source_t test_exception_other_t anon_inode",
164 "todo_deny b/101 test_undefined_source_t test_exception_target_t file",
166 "todo_deny b/102 test_exception_source_t test_undefined_target_t file",
167 ];
168
169 struct TestData {
170 policy: Arc<Policy<ByValue<Vec<u8>>>>,
171 defined_source: TypeId,
172 defined_target: TypeId,
173 unmatched_type: TypeId,
174 }
175
176 fn test_data() -> TestData {
177 let (parsed, _) = parse_policy_by_value(TEST_POLICY.to_vec()).unwrap();
178 let policy = Arc::new(parsed.validate().unwrap());
179 let defined_source = policy.type_id_by_name(EXCEPTION_SOURCE_TYPE).unwrap();
180 let defined_target = policy.type_id_by_name(EXCEPTION_TARGET_TYPE).unwrap();
181 let unmatched_type = policy.type_id_by_name(UNMATCHED_TYPE).unwrap();
182
183 assert!(policy.type_id_by_name("test_undefined_source_t").is_none());
184 assert!(policy.type_id_by_name("test_undefined_target_t").is_none());
185
186 TestData { policy, defined_source, defined_target, unmatched_type }
187 }
188
189 #[test]
190 fn empty_config_is_valid() {
191 let _ = ExceptionsConfig::new(&test_data().policy, &[])
192 .expect("Empty exceptions config is valid");
193 }
194
195 #[test]
196 fn extra_separating_whitespace_is_valid() {
197 let _ = ExceptionsConfig::new(
198 &test_data().policy,
199 &["
200 todo_deny b/001\ttest_exception_source_t test_exception_target_t file
201 "],
202 )
203 .expect("Config with extra separating whitespace is valid");
204 }
205
206 #[test]
207 fn only_defined_types_resolve_to_lookup_entries() {
208 let test_data = test_data();
209
210 let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
211 .expect("Config with unresolved types is valid");
212
213 assert_eq!(config.todo_deny_entries.len(), 3);
214 }
215
216 #[test]
217 fn lookup_matching() {
218 let test_data = test_data();
219
220 let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
221 .expect("Config with unresolved types is valid");
222
223 assert_eq!(
225 config.lookup(test_data.defined_source, test_data.defined_target, KernelClass::File),
226 Some(NonZeroU64::new(1).unwrap())
227 );
228
229 assert_eq!(
231 config.lookup(test_data.defined_source, test_data.defined_target, KernelClass::Dir),
232 None
233 );
234 assert_eq!(
235 config.lookup(test_data.unmatched_type, test_data.defined_target, KernelClass::File),
236 None
237 );
238 assert_eq!(
239 config.lookup(test_data.defined_source, test_data.unmatched_type, KernelClass::File),
240 None
241 );
242 }
243}