Skip to main content

selinux/policy/
index.rs

1// Copyright 2024 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 super::security_context::SecurityContext;
6use super::{
7    AccessDecision, AccessVector, ClassId, MlsLevel, ParsedPolicy, PermissionId, RoleId, TypeId,
8};
9use crate::new_policy::rules::{HasRuleKey, RuleKind};
10use crate::new_policy::traits::{HasName, HasPolicyId};
11use crate::new_policy::{
12    CategorySet, Class, ClassDefault, ClassDefaultRange, CommonSymbol, FsUseType, GenfsConPath,
13    HandleUnknown, IdAndNameIndexed, SymbolArray,
14};
15use crate::{
16    ClassPermission as _, KernelClass, KernelPermission, NullessByteStr, PolicyCap,
17    ProcessPermission,
18};
19
20use std::collections::HashMap;
21use std::ops::Deref;
22
23use strum::VariantArray as _;
24
25/// The [`SecurityContext`] and [`FsUseType`] derived from some `fs_use_*` line of the policy.
26pub struct FsUseLabelAndType {
27    pub context: SecurityContext,
28    pub use_type: FsUseType,
29}
30
31/// Array of `PermissionId` values each of a kernel security class' permissions.
32type KernelPermissionIdsArray = [Option<PermissionId>; 32];
33
34/// An index for facilitating fast lookup of common abstractions inside parsed binary policy data
35/// structures. Typically, data is indexed by an enum that describes a well-known value and the
36/// index stores the offset of the data in the binary policy to avoid scanning a collection to find
37/// an element that contains a matching string. For example, the policy contains a collection of
38/// classes that are identified by string names included in each collection entry. However,
39/// `policy_index.classes(KernelClass::Process).unwrap()` yields the offset in the policy's
40/// collection of classes where the "process" class resides.
41#[derive(Debug)]
42pub struct PolicyIndex {
43    /// Map from [`KernelClass`]es to their corresponding [`ClassId`]s in the associated policy's
44    /// [`super::symbols::Classes`] collection.
45    classes: HashMap<KernelClass, ClassId>,
46    /// Index mapping kernel class permissions to their policy-specific `AccessVector` bit index.
47    permissions: [KernelPermissionIdsArray; KernelClass::VARIANTS.len()],
48    /// The parsed binary policy.
49    parsed_policy: ParsedPolicy,
50    /// The "object_r" role used as a fallback for new file context transitions.
51    cached_object_r_role: RoleId,
52    /// The cached `ClassId` for the "process" class, if defined by the policy.
53    cached_process_class: Option<ClassId>,
54}
55
56impl PolicyIndex {
57    /// Constructs a [`PolicyIndex`] that indexes over well-known policy elements.
58    ///
59    /// [`Class`]es and [`Permission`]s used by the kernel are amongst the indexed elements.
60    /// The policy's `handle_unknown()` configuration determines whether the policy can be loaded even
61    /// if it omits classes or permissions expected by the kernel, and whether to allow or deny those
62    /// permissions if so.
63    pub fn new(parsed_policy: ParsedPolicy) -> Result<Self, anyhow::Error> {
64        let policy_classes = parsed_policy.classes();
65        let common_symbols = parsed_policy.common_symbols();
66
67        let mut classes = HashMap::with_capacity(crate::KernelClass::VARIANTS.len());
68
69        // Insert elements for each kernel object class. If the policy defines that unknown
70        // kernel classes should cause rejection then return an error describing the missing
71        // element.
72        for known_class in crate::KernelClass::VARIANTS {
73            match policy_classes.get_by_name(known_class.name().as_bytes()) {
74                Some(class) => {
75                    classes.insert(*known_class, class.id());
76                }
77                None => {
78                    if parsed_policy.handle_unknown() == HandleUnknown::Reject {
79                        return Err(anyhow::anyhow!("missing object class {:?}", known_class,));
80                    }
81                }
82            }
83        }
84
85        // Allow unused space in the classes map to be released.
86        classes.shrink_to_fit();
87
88        // Accumulate permissions indexed by kernel permission enum. If the policy defines that
89        // unknown permissions or classes should cause rejection then return an error describing the
90        // missing element.
91        let mut permissions = [KernelPermissionIdsArray::default(); _];
92        for kernel_permission in crate::KernelPermission::all_variants() {
93            let kernel_class_name = kernel_permission.class().name();
94            if let Some(class) = policy_classes.get_by_name(kernel_class_name.as_bytes()) {
95                if let Some(permission_id) =
96                    get_permission_id_by_name(common_symbols, class, kernel_permission.name())
97                {
98                    let kernel_class_id = kernel_permission.class() as usize;
99                    let kernel_permission_id = kernel_permission.id() as usize;
100                    permissions[kernel_class_id][kernel_permission_id] = Some(permission_id);
101                } else if parsed_policy.handle_unknown() == HandleUnknown::Reject {
102                    return Err(anyhow::anyhow!(
103                        "missing permission {:?}:{:?}",
104                        kernel_class_name,
105                        kernel_permission.name(),
106                    ));
107                }
108            }
109        }
110
111        // Locate the "object_r" role.
112        let cached_object_r_role = parsed_policy
113            .roles()
114            .get_by_name(b"object_r")
115            .ok_or_else(|| anyhow::anyhow!("missing 'object_r' role"))?
116            .id();
117
118        let cached_process_class = classes.get(&KernelClass::Process).copied();
119
120        let index = Self {
121            classes,
122            permissions,
123            parsed_policy,
124            cached_object_r_role,
125            cached_process_class,
126        };
127
128        Ok(index)
129    }
130
131    /// Returns the policy entry for a class identified either by its well-known kernel object class
132    /// enum value, or its policy-defined Id.
133    pub(super) fn class(&self, object_class: crate::ObjectClass) -> Option<&Class> {
134        match object_class {
135            crate::ObjectClass::Kernel(kernel_class) => {
136                let &class_id = self.classes.get(&kernel_class)?;
137                self.classes().get_by_id(class_id)
138            }
139            crate::ObjectClass::ClassId(class_id) => self.classes().get_by_id(class_id),
140        }
141    }
142
143    /// Returns the policy entry for a well-known kernel object class permission.
144    pub fn kernel_permission_to_access_vector<P: Into<KernelPermission>>(
145        &self,
146        permission: P,
147    ) -> Option<AccessVector> {
148        let permission = permission.into();
149        let class_index = permission.class() as usize;
150        let permission_index = permission.id() as usize;
151        let permission_id = self.permissions[class_index][permission_index]?;
152        Some(permission_id.into())
153    }
154
155    /// Returns the security context that should be applied to a newly created SELinux
156    /// object according to `source` and `target` security contexts, as well as the new object's
157    /// `class` and `name`.
158    ///
159    /// Computation follows the "create" algorithm for labeling newly created objects:
160    /// - user is taken from the `source`.
161    /// - role, type and range are taken from the matching transition rules, if any.
162    /// - role, type and range fall-back to the `source` or `target` values according to policy.
163    ///
164    /// Callers pass an empty slice (`&[]`) for `name` to express nameless transitions.
165    /// When a non-empty `name` is provided, filename transition rules are checked first.
166    /// If no transitions apply, and the policy does not explicitly specify defaults then the
167    /// role, type and range values have defaults chosen based on the `class`:
168    /// - For "process", and socket-like classes, role, type and range are taken from the `source`.
169    /// - Otherwise role is "object_r", type is taken from `target` and range is set to the
170    ///   low level of the `source` range.
171    pub fn compute_create_context(
172        &self,
173        source: &SecurityContext,
174        target: &SecurityContext,
175        class: crate::ObjectClass,
176        name: &[u8],
177    ) -> SecurityContext {
178        let override_type = if !name.is_empty() {
179            self.class(class).and_then(|policy_class| {
180                self.type_transition_new_type_with_name(
181                    source.type_(),
182                    target.type_(),
183                    &policy_class,
184                    name,
185                )
186            })
187        } else {
188            None
189        };
190        self.new_security_context_internal(source, target, class, override_type)
191    }
192
193    /// Internal implementation used by [`Self::compute_create_context`] to implement the policy transition calculations.
194    /// If `override_type` is specified then the supplied value will be applied rather than a value
195    /// being calculated based on the policy; this is used by [`Self::compute_create_context`]
196    /// when a filename transition matches to shortcut the default `type_transition` lookup.
197    fn new_security_context_internal(
198        &self,
199        source: &SecurityContext,
200        target: &SecurityContext,
201        target_class: crate::ObjectClass,
202        override_type: Option<TypeId>,
203    ) -> SecurityContext {
204        let Some(policy_class) = self.class(target_class) else {
205            // If the class is not defined in the policy then there can be no transitions, nor
206            // class-defined choice of defaults, so default to the non-process-or-socket behaviour.
207            // TODO: https://fxbug.dev/361552580 - For `KernelClass`es, apply the kernel's notion
208            // of whether the class is "process", or socket-like?
209            return SecurityContext::new(
210                source.user(),
211                self.cached_object_r_role,
212                target.type_(),
213                source.low_level().clone(),
214                None,
215            );
216        };
217
218        let is_process_or_socket =
219            policy_class.name() == b"process" || policy_class.common_name() == b"socket";
220        let (unspecified_role, unspecified_type, unspecified_low, unspecified_high) =
221            if is_process_or_socket {
222                (source.role(), source.type_(), source.low_level(), source.high_level())
223            } else {
224                (self.cached_object_r_role, target.type_(), source.low_level(), None)
225            };
226        let class_defaults = policy_class.defaults();
227
228        let user = match class_defaults.user() {
229            ClassDefault::Source => source.user(),
230            ClassDefault::Target => target.user(),
231            ClassDefault::Unspecified => source.user(),
232        };
233
234        let role = match self.role_transition_new_role(source.role(), target.type_(), &policy_class)
235        {
236            Some(new_role) => new_role,
237            None => match class_defaults.role() {
238                ClassDefault::Source => source.role(),
239                ClassDefault::Target => target.role(),
240                ClassDefault::Unspecified => unspecified_role,
241            },
242        };
243
244        let type_ = override_type.unwrap_or_else(|| {
245            let transition = self
246                .access_vector_rules()
247                .find_type_rules(source.type_(), target.type_(), policy_class.id())
248                .find(|rule| rule.kind() == RuleKind::TypeTransition)
249                .map(|rule| rule.new_type());
250            match transition {
251                Some(new_type) => new_type,
252                None => match class_defaults.type_() {
253                    ClassDefault::Source => source.type_(),
254                    ClassDefault::Target => target.type_(),
255                    ClassDefault::Unspecified => unspecified_type,
256                },
257            }
258        });
259
260        let (low_level, high_level) =
261            match self.range_transition_new_range(source.type_(), target.type_(), &policy_class) {
262                Some((low_level, high_level)) => (low_level, high_level),
263                None => match class_defaults.range() {
264                    ClassDefaultRange::SourceLow => (source.low_level().clone(), None),
265                    ClassDefaultRange::SourceHigh => {
266                        (source.high_level().unwrap_or_else(|| source.low_level()).clone(), None)
267                    }
268                    ClassDefaultRange::SourceLowHigh => {
269                        (source.low_level().clone(), source.high_level().cloned())
270                    }
271                    ClassDefaultRange::TargetLow => (target.low_level().clone(), None),
272                    ClassDefaultRange::TargetHigh => {
273                        (target.high_level().unwrap_or_else(|| target.low_level()).clone(), None)
274                    }
275                    ClassDefaultRange::TargetLowHigh => {
276                        (target.low_level().clone(), target.high_level().cloned())
277                    }
278                    ClassDefaultRange::Unspecified => {
279                        (unspecified_low.clone(), unspecified_high.cloned())
280                    }
281                    ClassDefaultRange::Glblub => glblub_range(source, target),
282                },
283            };
284
285        // TODO(http://b/334968228): Validate domain & role transitions are allowed?
286        SecurityContext::new(user, role, type_, low_level, high_level)
287    }
288
289    /// Evaluates the access rights allowed, and whether an audit should be emitted for any allowed
290    /// or denied permissions, by `source_context` acting on `target_context` as `target_class`.
291    pub(super) fn compute_access_decision(
292        &self,
293        source_context: &SecurityContext,
294        target_context: &SecurityContext,
295        target_class: &Class,
296    ) -> AccessDecision {
297        let mut access_decision = self.parsed_policy.compute_access_decision(
298            source_context,
299            target_context,
300            target_class,
301        );
302
303        // Process domain transitions ("transition" and "dyntransition") across different roles
304        // require explicit authorization in policy via a role allow rule ("allow old_role new_role;").
305        if source_context.role() != target_context.role()
306            && Some(target_class.id()) == self.cached_process_class
307        {
308            let process_trans_perms = self.process_trans_perms();
309            if (access_decision.allow & process_trans_perms) != AccessVector::NONE
310                && !self.role_transition_is_explicitly_allowed(
311                    source_context.role(),
312                    target_context.role(),
313                )
314            {
315                // The source is granted one or both of the "transition" permissions, but the role
316                // transition is not explicitly allowed, so remove those permissions from the
317                // returned decision.
318                access_decision.allow -= process_trans_perms;
319            }
320        }
321
322        access_decision
323    }
324
325    /// Returns the combined permissions mask for process `transition` and `dyntransition`.
326    fn process_trans_perms(&self) -> AccessVector {
327        let mut perms = self
328            .kernel_permission_to_access_vector(ProcessPermission::Transition)
329            .unwrap_or(AccessVector::NONE);
330        perms |= self
331            .kernel_permission_to_access_vector(ProcessPermission::DynTransition)
332            .unwrap_or(AccessVector::NONE);
333        perms
334    }
335
336    /// Returns the Id of the "object_r" role within the `parsed_policy`, for use when validating
337    /// Security Context fields.
338    pub(super) fn object_role(&self) -> RoleId {
339        self.cached_object_r_role
340    }
341
342    /// Returns the [`SecurityContext`] defined by this policy for the specified
343    /// well-known (or "initial") Id.
344    pub(super) fn initial_context(&self, id: crate::InitialSid) -> SecurityContext {
345        // All [`InitialSid`] have already been verified as resolvable, by `new()`.
346        SecurityContext::from_policy_context(self.parsed_policy.initial_context(id))
347    }
348
349    /// If there is an fs_use statement for the given filesystem type, returns the associated
350    /// [`SecurityContext`] and [`FsUseType`].
351    pub(super) fn fs_use_label_and_type(
352        &self,
353        fs_type: NullessByteStr<'_>,
354    ) -> Option<FsUseLabelAndType> {
355        self.object_contexts()
356            .fs_uses()
357            .iter()
358            .find(|fs_use| fs_use.fs_type() == fs_type.as_bytes())
359            .map(|fs_use| FsUseLabelAndType {
360                context: SecurityContext::from_policy_context(fs_use.context()),
361                use_type: fs_use.behavior(),
362            })
363    }
364
365    /// If there is a genfscon statement for the given filesystem type, returns the associated
366    /// [`SecurityContext`], taking the `node_path` into account. `class_id` defines the type
367    /// of the file in the given `node_path`. It can only be omitted when looking up the filesystem
368    /// label.
369    pub(super) fn genfscon_label_for_fs_and_path(
370        &self,
371        fs_type: NullessByteStr<'_>,
372        node_path: NullessByteStr<'_>,
373        class: Option<crate::KernelClass>,
374    ) -> Option<SecurityContext> {
375        let node_path = if class == Some(crate::FileClass::LnkFile.into())
376            && !self.has_policycap(PolicyCap::GenfsSeclabelSymlinks)
377        {
378            // Symlinks receive the filesystem root label by default, rather than a label dependent on
379            // the `node_path`. Path based labels may be enabled with the "genfs_seclabel_symlinks"
380            // policy capability.
381            "/".into()
382        } else {
383            node_path
384        };
385
386        let class_id = class.and_then(|class| self.class(class.into())).map(|class| class.id());
387
388        // All contexts listed in the policy for the file system type.
389        let fs_contexts = self.genfscon_find_all(fs_type.as_bytes());
390
391        #[derive(PartialEq)]
392        enum OrderType {
393            Alphabetic,
394            ByLength,
395            Unknown,
396        }
397        // The correct match is the closest parent among the ones given in the policy file.
398        // E.g. if in the policy we have
399        //     genfscon foofs "/" label1
400        //     genfscon foofs "/abc/" label2
401        //     genfscon foofs "/abc/def" label3
402        //
403        // The correct label for a file "/abc/def/g/h/i" is label3, as "/abc/def" is the closest parent
404        // among those defined.
405        //
406        // Partial paths are prefix-matched, so that "/abc/default" would also be assigned label3.
407        //
408        // TODO(372212126): Optimize the algorithm.
409        let mut result: Option<&GenfsConPath> = None;
410        let mut order_type = OrderType::Unknown;
411        let mut prev_path_bytes: Option<Vec<u8>> = None;
412        for fs_context in fs_contexts {
413            // Determine the order type based on the first entries.
414            let path = fs_context.partial_path();
415            if order_type == OrderType::Unknown {
416                if let Some(prev) = &prev_path_bytes {
417                    if path.len() > prev.len() {
418                        order_type = OrderType::Alphabetic;
419                    } else if path < prev.as_slice() {
420                        order_type = OrderType::ByLength;
421                    }
422                }
423                prev_path_bytes = Some(path.to_vec());
424            }
425
426            // Check if the class matches.
427            let class_matches = class_id.is_none()
428                || fs_context
429                    .class()
430                    .map(|other| other == class_id.unwrap().into())
431                    .unwrap_or(true);
432            if !class_matches {
433                continue;
434            }
435
436            if order_type == OrderType::Alphabetic && fs_context.partial_path() > node_path.0 {
437                // We know that:
438                // - We have alphabetic order,
439                // - The current path is lexicographically greater than our target path.
440                // We can infer that we have passed any potential prefixes in alphabetical order.
441                break;
442            }
443
444            if node_path.0.starts_with(fs_context.partial_path()) {
445                if result
446                    .as_ref()
447                    .map_or(true, |c| c.partial_path().len() < fs_context.partial_path().len())
448                {
449                    // The path matches, and it's the closest parent so far.
450                    result = Some(fs_context);
451                    if order_type == OrderType::ByLength {
452                        break;
453                    }
454                }
455            }
456        }
457
458        // The returned SecurityContext must be valid with respect to the policy, since otherwise
459        // we'd have rejected the policy load.
460        Some(SecurityContext::from_policy_context(result?.context()))
461    }
462
463    fn role_transition_new_role(
464        &self,
465        current_role: RoleId,
466        type_: TypeId,
467        class: &Class,
468    ) -> Option<RoleId> {
469        self.role_transitions()
470            .iter()
471            .find(|role_transition| {
472                role_transition.current_role() == current_role
473                    && role_transition.type_() == type_
474                    && role_transition.class() == class.id().into()
475            })
476            .map(|x| x.new_role())
477    }
478
479    fn role_transition_is_explicitly_allowed(&self, source_role: RoleId, new_role: RoleId) -> bool {
480        self.role_allowlist().iter().any(|role_allow| {
481            role_allow.source_role() == source_role && role_allow.new_role() == new_role
482        })
483    }
484
485    fn type_transition_new_type_with_name(
486        &self,
487        source_type: TypeId,
488        target_type: TypeId,
489        class: &Class,
490        name: &[u8],
491    ) -> Option<TypeId> {
492        self.compute_filename_transition(source_type, target_type, class.id().into(), name)
493    }
494
495    fn range_transition_new_range(
496        &self,
497        source_type: TypeId,
498        target_type: TypeId,
499        class: &Class,
500    ) -> Option<(MlsLevel, Option<MlsLevel>)> {
501        for range_transition in self.range_transitions() {
502            if range_transition.source_type() == source_type
503                && range_transition.target_type() == target_type
504                && range_transition.target_class() == class.id()
505            {
506                let mls_range = range_transition.mls_range();
507                let low_level = mls_range.low().clone();
508                let high_level = mls_range.high().clone();
509                return Some((low_level, high_level));
510            }
511        }
512
513        None
514    }
515}
516
517/// Returns the bit index of the specified permission for the specified security `class`, looking
518/// up the permission in the class' common symbol, if any.
519fn get_permission_id_by_name(
520    common_symbols: &IdAndNameIndexed<SymbolArray<CommonSymbol>>,
521    class: &Class,
522    name: &str,
523) -> Option<PermissionId> {
524    let name = name.as_bytes();
525    if let Some(permission) = class.permissions().iter().find(|p| p.name() == name) {
526        return Some(permission.id());
527    }
528    let common_name = class.common_name();
529    if !common_name.is_empty() {
530        let common_symbol = common_symbols.get_by_name(common_name)?;
531        let permission = common_symbol.permissions().iter().find(|p| p.name() == name)?;
532        return Some(permission.id());
533    }
534    None
535}
536
537/// Returns the set of categories present in both `left` and `right`.
538fn intersect_categories(left: &MlsLevel, right: &MlsLevel) -> CategorySet {
539    let right_categories = right.categories();
540    CategorySet::from_ids(left.category_ids().filter(|id| right_categories.contains(*id)))
541}
542
543/// Returns the intersection of the `source` and `target` ranges.
544///
545/// If the two ranges do not overlap then the intersection is empty, and the returned range is
546/// mis-ordered, i.e. its high level does not dominate its low level. Such a range is rejected
547/// when the resulting Security Context is validated.
548fn glblub_range(
549    source: &SecurityContext,
550    target: &SecurityContext,
551) -> (MlsLevel, Option<MlsLevel>) {
552    let source_low = source.low_level();
553    let source_high = source.effective_high_level();
554    let target_low = target.low_level();
555    let target_high = target.effective_high_level();
556
557    let low_level = MlsLevel::new(
558        std::cmp::max(source_low.sensitivity(), target_low.sensitivity()),
559        intersect_categories(source_low, target_low),
560    );
561    let high_level = MlsLevel::new(
562        std::cmp::min(source_high.sensitivity(), target_high.sensitivity()),
563        intersect_categories(source_high, target_high),
564    );
565    (low_level, Some(high_level))
566}
567
568impl Deref for PolicyIndex {
569    type Target = ParsedPolicy;
570
571    fn deref(&self) -> &Self::Target {
572        &self.parsed_policy
573    }
574}