selinux/
exceptions_config.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::policy::{Policy, TypeId};
6use crate::{KernelClass, ObjectClass};
7
8use anyhow::{anyhow, bail};
9use std::collections::HashMap;
10use std::num::NonZeroU64;
11
12/// Encapsulates a set of access-check exceptions parsed from a supplied configuration.
13pub(super) struct ExceptionsConfig {
14    todo_deny_entries: HashMap<ExceptionsEntry, NonZeroU64>,
15    permissive_entries: HashMap<TypeId, NonZeroU64>,
16}
17
18impl ExceptionsConfig {
19    /// Parses the supplied `exceptions` lines and returns an `ExceptionsConfig` with an entry for
20    /// each parsed exception definition. If a definition's source or target type/domain are not
21    /// defined by the supplied `policy` then the entry is ignored, so that removal/renaming of
22    /// policy elements will not break the exceptions configuration.
23    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    /// Returns the non-zero integer bug Id for the exception associated with the specified source,
36    /// target and class, if any.
37    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                    // "todo_deny" lines have the form:
55                    //   todo_deny b/<id> <source> <target> <class>
56
57                    // Parse the bug Id, which must be present
58                    let bug_id = bug_ref_to_id(
59                        parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
60                    )?;
61
62                    // Parse the source & target types. If either of these is not defined by the
63                    // `policy` then the statement is ignored.
64                    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                    // Parse the object class name to the corresponding policy-specific Id.
74                    // This allows non-kernel classes, and userspace queries against kernel classes,
75                    // to have exceptions applied to them.
76                    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                    // Parse the kernel object class. This must correspond to a known kernel object
83                    // class, regardless of whether the policy actually defines the class.
84                    let kernel_class = object_class_by_name(class_name);
85
86                    // If the class isn't defined by policy, or used by the kernel, then there is
87                    // no way to apply the exception.
88                    if policy_class.is_none() && kernel_class.is_none() {
89                        println!("Ignoring statement: {} (unknown class)", line);
90                        return Ok(());
91                    }
92
93                    // If the source or target domains are unrecognized then there is no way to
94                    // apply the exception.
95                    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                    // "todo_permissive" lines have the form:
115                    //   todo_permissive b/<id> <source>
116
117                    // Parse the bug Id, which must be present
118                    let bug_id = bug_ref_to_id(
119                        parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
120                    )?;
121
122                    // Parse the source type. The statement is ignored if the type is not defined by policy.
123                    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/// Key used to index the access check exceptions table.
141#[derive(Eq, Hash, PartialEq)]
142struct ExceptionsEntry {
143    source: TypeId,
144    target: TypeId,
145    class: ObjectClass,
146}
147
148/// Returns the numeric bug Id parsed from a bug URL reference.
149fn 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
159/// Returns the `KernelClass` corresponding to the supplied `name`, if any.
160/// `None` is returned if no such kernel object class exists in the Starnix implementation.
161fn 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        // These statements should resolve into both kernel-Id and policy-Id indexed entries.
183        "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        // This statement should resolve into a kernel-Id indexed entry, because neither the "base"
186        // policy fragment, nor the exceptions test fragment, define the `anon_inode` class.
187        "todo_deny b/003 test_exception_source_t test_exception_other_t anon_inode",
188        // This statement should resolve into a policy-Id indexed entry, because the class is not
189        // one known to the kernel.
190        "todo_deny b/004 test_exception_source_t test_exception_target_t test_exception_non_kernel_class",
191        // These statements should not be resolved.
192        "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        // Matching source, target & kernel class will resolve to the corresponding bug Id.
263        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        // Matching source, target and kernel class identified via policy-defined Id will resolve to
273        // the same bug Id as if looked up via the kernel enum.
274        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        // Matching source, target and non-kernel class will resolve.
284        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        // Mismatched class, source or target returns no Id.
294        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}