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