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::parser::ByValue;
6use crate::policy::{Policy, TypeId};
7use crate::ObjectClass;
8
9use anyhow::{anyhow, bail};
10use std::collections::HashMap;
11use std::num::NonZeroU64;
12
13/// Encapsulates a set of access-check exceptions parsed from a supplied configuration.
14pub(super) struct ExceptionsConfig {
15    entries: HashMap<ExceptionsEntry, NonZeroU64>,
16}
17
18impl ExceptionsConfig {
19    /// Parses the supplied `exceptions_config` 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(
24        policy: &Policy<ByValue<Vec<u8>>>,
25        exceptions_config: &str,
26    ) -> Result<Self, anyhow::Error> {
27        let lines = exceptions_config.lines();
28        let mut result = Self { entries: HashMap::with_capacity(lines.clone().count()) };
29        for line in lines {
30            result.parse_config_line(policy, line)?;
31        }
32        result.entries.shrink_to_fit();
33        Ok(result)
34    }
35
36    /// Returns the non-zero integer bug Id for the exception associated with the specified source,
37    /// target and class, if any.
38    pub(super) fn lookup(
39        &self,
40        source: TypeId,
41        target: TypeId,
42        class: ObjectClass,
43    ) -> Option<NonZeroU64> {
44        self.entries.get(&ExceptionsEntry { source, target, class }).copied()
45    }
46
47    fn parse_config_line(
48        &mut self,
49        policy: &Policy<ByValue<Vec<u8>>>,
50        line: &str,
51    ) -> Result<(), anyhow::Error> {
52        let mut parts = line.trim().split_whitespace();
53        if let Some(statement) = parts.next() {
54            match statement {
55                "todo_deny" => {
56                    // "todo_deny" lines have the form:
57                    //   todo_deny b/<id> <source> <target> <class>
58
59                    // Parse the bug Id, which must be present, prefixed by "b/"
60                    let bug = parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?;
61                    let bug_id_part = bug.strip_prefix("b/").or_else(|| bug.strip_prefix("https://fxbug.dev/"))
62                        .ok_or_else(|| anyhow!("Expected bug Identifier of the form b/<id> or https://fxbug.dev/<id>"))?;
63                    let bug_id = bug_id_part
64                        .parse::<NonZeroU64>()
65                        .map_err(|_| anyhow!("Malformed bug Id: {}", bug_id_part))?;
66
67                    // Parse the source & target types. If either of these is not defined by the
68                    // `policy` then the statement is ignored.
69                    let stype = policy.type_id_by_name(
70                        parts.next().ok_or_else(|| anyhow!("Expected source type"))?,
71                    );
72                    let ttype = policy.type_id_by_name(
73                        parts.next().ok_or_else(|| anyhow!("Expected target type"))?,
74                    );
75
76                    // Parse the kernel object class. This must correspond to a known kernel object
77                    // class, regardless of whether the policy actually defines the class.
78                    let class = parts
79                        .next()
80                        .and_then(object_class_by_name)
81                        .ok_or_else(|| anyhow!("Target class missing or unrecognized"))?;
82
83                    if let (Some(source), Some(target)) = (stype, ttype) {
84                        self.entries.insert(ExceptionsEntry { source, target, class }, bug_id);
85                    } else {
86                        println!("Ignoring statement: {}", line);
87                    }
88                }
89                "" | "//" => {}
90                _ => bail!("Unknown statement {}", statement),
91            }
92        }
93        Ok(())
94    }
95}
96
97/// Key used to index the access check exceptions table.
98#[derive(Eq, Hash, PartialEq)]
99struct ExceptionsEntry {
100    source: TypeId,
101    target: TypeId,
102    class: ObjectClass,
103}
104
105/// Returns the kernel `ObjectClass` corresponding to the supplied `name`, if any.
106/// `None` is returned if no such kernel object class exists in the Starnix implementation.
107fn object_class_by_name(name: &str) -> Option<ObjectClass> {
108    ObjectClass::all_variants().into_iter().find(|class| class.name() == name)
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::policy::parse_policy_by_value;
115    use std::sync::Arc;
116
117    const TEST_POLICY: &[u8] =
118        include_bytes!("../testdata/composite_policies/compiled/exceptions_config_policy.pp");
119
120    const EXCEPTION_SOURCE_TYPE: &str = "test_exception_source_t";
121    const EXCEPTION_TARGET_TYPE: &str = "test_exception_target_t";
122    const _EXCEPTION_OTHER_TYPE: &str = "test_exception_other_t";
123    const UNMATCHED_TYPE: &str = "test_exception_unmatched_t";
124
125    struct TestData {
126        policy: Arc<Policy<ByValue<Vec<u8>>>>,
127        defined_source: TypeId,
128        defined_target: TypeId,
129        unmatched_type: TypeId,
130    }
131
132    fn test_data() -> TestData {
133        let (parsed, _) = parse_policy_by_value(TEST_POLICY.to_vec()).unwrap();
134        let policy = Arc::new(parsed.validate().unwrap());
135        let defined_source = policy.type_id_by_name(EXCEPTION_SOURCE_TYPE).unwrap();
136        let defined_target = policy.type_id_by_name(EXCEPTION_TARGET_TYPE).unwrap();
137        let unmatched_type = policy.type_id_by_name(UNMATCHED_TYPE).unwrap();
138
139        assert!(policy.type_id_by_name("test_undefined_source_t").is_none());
140        assert!(policy.type_id_by_name("test_undefined_target_t").is_none());
141
142        TestData { policy, defined_source, defined_target, unmatched_type }
143    }
144
145    #[test]
146    fn empty_config_is_valid() {
147        let _ = ExceptionsConfig::new(&test_data().policy, "")
148            .expect("Empty exceptions config is valid");
149    }
150
151    #[test]
152    fn comments_and_empty_lines_are_valid() {
153        let _ = ExceptionsConfig::new(
154            &test_data().policy,
155            "
156            // This is a comment.
157
158            // This is a second comment, with a blank line preceding it.
159            ",
160        )
161        .expect("Config with only comments is valid");
162    }
163
164    #[test]
165    fn extra_separating_whitespace_is_valid() {
166        let _ = ExceptionsConfig::new(
167            &test_data().policy,
168            "
169            todo_deny b/001\ttest_exception_source_t     test_exception_target_t   file
170            ",
171        )
172        .expect("Config with extra separating whitespace is valid");
173    }
174
175    const TEST_CONFIG: &str = "
176            // These statement should all be resolved.
177            todo_deny b/001 test_exception_source_t test_exception_target_t file
178            todo_deny b/002 test_exception_other_t test_exception_target_t chr_file
179            todo_deny b/003 test_exception_source_t test_exception_other_t anon_inode
180
181            // These statements should not be resolved.
182            todo_deny b/101 test_undefined_source_t test_exception_target_t file
183            todo_deny b/102 test_exception_source_t test_undefined_target_t file
184        ";
185
186    #[test]
187    fn only_defined_types_resolve_to_lookup_entries() {
188        let test_data = test_data();
189
190        let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
191            .expect("Config with unresolved types is valid");
192
193        assert_eq!(config.entries.len(), 3);
194    }
195
196    #[test]
197    fn lookup_matching() {
198        let test_data = test_data();
199
200        let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
201            .expect("Config with unresolved types is valid");
202
203        // Matching source, target & class will resolve to the corresponding bug Id.
204        assert_eq!(
205            config.lookup(test_data.defined_source, test_data.defined_target, ObjectClass::File),
206            Some(NonZeroU64::new(1).unwrap())
207        );
208
209        // Mismatched class, source or target returns no Id.
210        assert_eq!(
211            config.lookup(test_data.defined_source, test_data.defined_target, ObjectClass::Dir),
212            None
213        );
214        assert_eq!(
215            config.lookup(test_data.unmatched_type, test_data.defined_target, ObjectClass::File),
216            None
217        );
218        assert_eq!(
219            config.lookup(test_data.defined_source, test_data.unmatched_type, ObjectClass::File),
220            None
221        );
222    }
223}