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