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