Skip to main content

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::new_policy::traits::HasPolicyId;
6use crate::policy::{Policy, TypeId};
7use crate::{KernelClass, ObjectClass};
8
9use anyhow::{anyhow, bail};
10use std::collections::HashMap;
11use std::num::NonZeroU32;
12use strum::VariantArray as _;
13
14/// Encapsulates a set of access-check exceptions parsed from a supplied configuration.
15pub(super) struct ExceptionsConfig {
16    todo_deny_entries: HashMap<ExceptionsEntry, NonZeroU32>,
17    permissive_entries: HashMap<TypeId, NonZeroU32>,
18}
19
20impl ExceptionsConfig {
21    /// Parses the supplied `exceptions` lines and returns an `ExceptionsConfig` with an entry for
22    /// each parsed exception definition. If a definition's source or target type/domain are not
23    /// defined by the supplied `policy` then the entry is ignored, so that removal/renaming of
24    /// policy elements will not break the exceptions configuration.
25    pub(super) fn new(policy: &Policy, exceptions: &[&str]) -> Result<Self, anyhow::Error> {
26        let mut result = Self {
27            todo_deny_entries: HashMap::with_capacity(exceptions.len()),
28            permissive_entries: HashMap::new(),
29        };
30        for line in exceptions {
31            result.parse_config_line(policy, line)?;
32        }
33        result.todo_deny_entries.shrink_to_fit();
34        Ok(result)
35    }
36
37    /// Returns the non-zero integer bug Id for the exception associated with the specified source,
38    /// target and class, if any.
39    pub(super) fn lookup(
40        &self,
41        source: TypeId,
42        target: TypeId,
43        class: ObjectClass,
44    ) -> Option<NonZeroU32> {
45        self.todo_deny_entries
46            .get(&ExceptionsEntry { source, target, class })
47            .or_else(|| self.permissive_entries.get(&source))
48            .copied()
49    }
50
51    fn parse_config_line(&mut self, policy: &Policy, line: &str) -> 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
60                    let bug_id = bug_ref_to_id(
61                        parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
62                    )?;
63
64                    // Parse the source & target types. If either of these is not defined by the
65                    // `policy` then the statement is ignored.
66                    let stype = policy
67                        .types()
68                        .get_by_name(
69                            parts.next().ok_or_else(|| anyhow!("Expected source type"))?.as_bytes(),
70                        )
71                        .map(|t| t.id());
72                    let ttype = policy
73                        .types()
74                        .get_by_name(
75                            parts.next().ok_or_else(|| anyhow!("Expected target type"))?.as_bytes(),
76                        )
77                        .map(|t| t.id());
78
79                    let class_name = parts.next().ok_or_else(|| anyhow!("Target class missing"))?;
80
81                    // Parse the object class name to the corresponding policy-specific Id.
82                    // This allows non-kernel classes, and userspace queries against kernel classes,
83                    // to have exceptions applied to them.
84                    let policy_class =
85                        policy.classes().get_by_name(class_name.as_bytes()).map(|x| x.id());
86
87                    // Parse the kernel object class. This must correspond to a known kernel object
88                    // class, regardless of whether the policy actually defines the class.
89                    let kernel_class = object_class_by_name(class_name);
90
91                    // If the class isn't defined by policy, or used by the kernel, then there is
92                    // no way to apply the exception.
93                    if policy_class.is_none() && kernel_class.is_none() {
94                        println!("Ignoring statement: {} (unknown class)", line);
95                        return Ok(());
96                    }
97
98                    // If the source or target domains are unrecognized then there is no way to
99                    // apply the exception.
100                    let (Some(source), Some(target)) = (stype, ttype) else {
101                        println!("Ignoring statement: {} (unknown source or target)", line);
102                        return Ok(());
103                    };
104
105                    if let Some(policy_class) = policy_class {
106                        self.todo_deny_entries.insert(
107                            ExceptionsEntry { source, target, class: policy_class.into() },
108                            bug_id,
109                        );
110                    }
111                    if let Some(kernel_class) = kernel_class {
112                        self.todo_deny_entries.insert(
113                            ExceptionsEntry { source, target, class: kernel_class.into() },
114                            bug_id,
115                        );
116                    }
117                }
118                "todo_permissive" => {
119                    // "todo_permissive" lines have the form:
120                    //   todo_permissive b/<id> <source>
121
122                    // Parse the bug Id, which must be present
123                    let bug_id = bug_ref_to_id(
124                        parts.next().ok_or_else(|| anyhow!("Expected bug identifier"))?,
125                    )?;
126
127                    // Parse the source type. The statement is ignored if the type is not defined by policy.
128                    let stype = policy
129                        .types()
130                        .get_by_name(
131                            parts.next().ok_or_else(|| anyhow!("Expected source type"))?.as_bytes(),
132                        )
133                        .map(|t| t.id());
134
135                    if let Some(source) = stype {
136                        self.permissive_entries.insert(source, bug_id);
137                    } else {
138                        println!("Ignoring statement: {}", line);
139                    }
140                }
141                _ => bail!("Unknown statement {}", statement),
142            }
143        }
144        Ok(())
145    }
146}
147
148/// Key used to index the access check exceptions table.
149#[derive(Eq, Hash, PartialEq)]
150struct ExceptionsEntry {
151    source: TypeId,
152    target: TypeId,
153    class: ObjectClass,
154}
155
156/// Returns the numeric bug Id parsed from a bug URL reference.
157fn bug_ref_to_id(bug_ref: &str) -> Result<NonZeroU32, anyhow::Error> {
158    let bug_id_part = bug_ref
159        .strip_prefix("b/")
160        .or_else(|| bug_ref.strip_prefix("https://fxbug.dev/"))
161        .ok_or_else(|| {
162            anyhow!("Expected bug Identifier of the form b/<id> or https://fxbug.dev/<id>")
163        })?;
164    bug_id_part.parse::<NonZeroU32>().map_err(|_| anyhow!("Malformed bug Id: {}", bug_id_part))
165}
166
167/// Returns the `KernelClass` corresponding to the supplied `name`, if any.
168/// `None` is returned if no such kernel object class exists in the Starnix implementation.
169fn object_class_by_name(name: &str) -> Option<KernelClass> {
170    KernelClass::VARIANTS.iter().find(|class| class.name() == name).map(Clone::clone)
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::policy::parse_policy_by_value;
177    use std::sync::Arc;
178
179    const TEST_POLICY: &[u8] =
180        include_bytes!("../testdata/composite_policies/compiled/exceptions_config_policy");
181
182    const EXCEPTION_SOURCE_TYPE: &[u8] = b"test_exception_source_t";
183    const EXCEPTION_TARGET_TYPE: &[u8] = b"test_exception_target_t";
184    const _EXCEPTION_OTHER_TYPE: &[u8] = b"test_exception_other_t";
185    const UNMATCHED_TYPE: &[u8] = b"test_exception_unmatched_t";
186
187    const NON_KERNEL_CLASS: &str = "test_exception_non_kernel_class";
188
189    const TEST_CONFIG: &[&str] = &[
190        // These statements should resolve into both kernel-Id and policy-Id indexed entries.
191        "todo_deny b/001 test_exception_source_t test_exception_target_t file",
192        "todo_deny b/002 test_exception_other_t test_exception_target_t chr_file",
193        // This statement should resolve into a kernel-Id indexed entry, because neither the "base"
194        // policy fragment, nor the exceptions test fragment, define the `anon_inode` class.
195        "todo_deny b/003 test_exception_source_t test_exception_other_t anon_inode",
196        // This statement should resolve into a policy-Id indexed entry, because the class is not
197        // one known to the kernel.
198        "todo_deny b/004 test_exception_source_t test_exception_target_t test_exception_non_kernel_class",
199        // These statements should not be resolved.
200        "todo_deny b/101 test_undefined_source_t test_exception_target_t file",
201        "todo_deny b/102 test_exception_source_t test_undefined_target_t file",
202        "todo_deny b/103 test_exception_source_t test_exception_target_t test_exception_non_existent_class",
203    ];
204
205    struct TestData {
206        policy: Arc<Policy>,
207        defined_source: TypeId,
208        defined_target: TypeId,
209        unmatched_type: TypeId,
210    }
211
212    impl TestData {
213        fn expect_policy_class(&self, name: &str) -> ObjectClass {
214            self.policy
215                .classes()
216                .get_by_name(name.as_bytes())
217                .map(|x| x.id())
218                .expect("Unable to resolve policy class Id")
219                .into()
220        }
221    }
222    fn test_data() -> TestData {
223        let parsed = parse_policy_by_value(TEST_POLICY.to_vec()).unwrap();
224        let policy = Arc::new(parsed.validate().unwrap());
225        let defined_source = policy.types().get_by_name(EXCEPTION_SOURCE_TYPE).unwrap().id();
226        let defined_target = policy.types().get_by_name(EXCEPTION_TARGET_TYPE).unwrap().id();
227        let unmatched_type = policy.types().get_by_name(UNMATCHED_TYPE).unwrap().id();
228
229        assert!(policy.types().get_by_name(b"test_undefined_source_t").is_none());
230        assert!(policy.types().get_by_name(b"test_undefined_target_t").is_none());
231
232        TestData { policy, defined_source, defined_target, unmatched_type }
233    }
234
235    #[test]
236    fn empty_config_is_valid() {
237        let _ = ExceptionsConfig::new(&test_data().policy, &[])
238            .expect("Empty exceptions config is valid");
239    }
240
241    #[test]
242    fn extra_separating_whitespace_is_valid() {
243        let _ = ExceptionsConfig::new(
244            &test_data().policy,
245            &["
246            todo_deny b/001\ttest_exception_source_t     test_exception_target_t   file
247    "],
248        )
249        .expect("Config with extra separating whitespace is valid");
250    }
251
252    #[test]
253    fn only_defined_types_resolve_to_lookup_entries() {
254        let test_data = test_data();
255
256        let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
257            .expect("Config with unresolved types is valid");
258
259        assert_eq!(config.todo_deny_entries.len(), 6);
260    }
261
262    #[test]
263    fn lookup_matching() {
264        let test_data = test_data();
265
266        let config = ExceptionsConfig::new(&test_data.policy, TEST_CONFIG)
267            .expect("Config with unresolved types is valid");
268
269        // Matching source, target & kernel class will resolve to the corresponding bug Id.
270        assert_eq!(
271            config.lookup(
272                test_data.defined_source,
273                test_data.defined_target,
274                KernelClass::File.into()
275            ),
276            Some(NonZeroU32::new(1).unwrap())
277        );
278
279        // Matching source, target and kernel class identified via policy-defined Id will resolve to
280        // the same bug Id as if looked up via the kernel enum.
281        assert_eq!(
282            config.lookup(
283                test_data.defined_source,
284                test_data.defined_target,
285                test_data.expect_policy_class("file")
286            ),
287            Some(NonZeroU32::new(1).unwrap())
288        );
289
290        // Matching source, target and non-kernel class will resolve.
291        assert_eq!(
292            config.lookup(
293                test_data.defined_source,
294                test_data.defined_target,
295                test_data.expect_policy_class(NON_KERNEL_CLASS),
296            ),
297            Some(NonZeroU32::new(4).unwrap())
298        );
299
300        // Mismatched class, source or target returns no Id.
301        assert_eq!(
302            config.lookup(
303                test_data.defined_source,
304                test_data.defined_target,
305                KernelClass::Dir.into()
306            ),
307            None
308        );
309        assert_eq!(
310            config.lookup(
311                test_data.unmatched_type,
312                test_data.defined_target,
313                KernelClass::File.into()
314            ),
315            None
316        );
317        assert_eq!(
318            config.lookup(
319                test_data.defined_source,
320                test_data.unmatched_type,
321                KernelClass::File.into()
322            ),
323            None
324        );
325    }
326}