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` lines 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: &[&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    /// Returns the non-zero integer bug Id for the exception associated with the specified source,
40    /// target and class, if any.
41    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                    // "todo_deny" lines have the form:
63                    //   todo_deny b/<id> <source> <target> <class>
64
65                    // Parse the bug Id, which must be present
66                    let bug_id = bug_ref_to_id(
67                        parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
68                    )?;
69
70                    // Parse the source & target types. If either of these is not defined by the
71                    // `policy` then the statement is ignored.
72                    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                    // Parse the kernel object class. This must correspond to a known kernel object
80                    // class, regardless of whether the policy actually defines the class.
81                    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                    // "todo_permissive" lines have the form:
95                    //   todo_permissive b/<id> <source>
96
97                    // Parse the bug Id, which must be present
98                    let bug_id = bug_ref_to_id(
99                        parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
100                    )?;
101
102                    // Parse the source type. The statement is ignored if the type is not defined by policy.
103                    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/// Key used to index the access check exceptions table.
121#[derive(Eq, Hash, PartialEq)]
122struct ExceptionsEntry {
123    source: TypeId,
124    target: TypeId,
125    class: KernelClass,
126}
127
128/// Returns the numeric bug Id parsed from a bug URL reference.
129fn 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
139/// Returns the `KernelClass` corresponding to the supplied `name`, if any.
140/// `None` is returned if no such kernel object class exists in the Starnix implementation.
141fn 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        // These statement should all be resolved.
161        "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        // These statements should not be resolved.
165        "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        // Matching source, target & class will resolve to the corresponding bug Id.
224        assert_eq!(
225            config.lookup(test_data.defined_source, test_data.defined_target, KernelClass::File),
226            Some(NonZeroU64::new(1).unwrap())
227        );
228
229        // Mismatched class, source or target returns no Id.
230        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}