blob: 66ce8918c93f01c6b0bffc6cbb57495fffa7653b [file] [log] [blame]
// Copyright 2024 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use super::{
error::NewSecurityContextError,
extensible_bitmap::ExtensibleBitmapSpan,
parser::ParseStrategy,
security_context::{self, SecurityLevel},
symbols::{
Class, ClassDefault, ClassDefaultRange, Classes, CommonSymbol, CommonSymbols, MlsLevel,
Permission,
},
CategoryId, ParsedPolicy, RoleId, SecurityContext, TypeId,
};
use selinux_common::{self as sc, ClassPermission as _};
use std::{collections::HashMap, num::NonZeroU32};
/// An index for facilitating fast lookup of common abstractions inside parsed binary policy data
/// structures. Typically, data is indexed by an enum that describes a well-known value and the
/// index stores the offset of the data in the binary policy to avoid scanning a collection to find
/// an element that contains a matching string. For example, the policy contains a collection of
/// classes that are identified by string names included in each collection entry. However,
/// `policy_index.classes(ObjectClass::Process).unwrap()` yields the offset in the policy's
/// collection of classes where the "process" class resides.
#[derive(Debug)]
pub(crate) struct PolicyIndex<PS: ParseStrategy> {
/// Map from well-known classes to their offsets in the associate policy's
/// [`crate::symbols::Classes`] collection.
classes: HashMap<sc::ObjectClass, usize>,
/// Map from well-known permissions to their class's associated [`crate::symbols::Permissions`]
/// collection.
permissions: HashMap<sc::Permission, PermissionIndex>,
/// The parsed binary policy.
parsed_policy: ParsedPolicy<PS>,
/// The "object_r" role used as a fallback for new file context transitions.
cached_object_r_role: RoleId,
}
impl<PS: ParseStrategy> PolicyIndex<PS> {
/// Constructs a [`PolicyIndex`] that indexes over well-known policy fragment names. For
/// example, the "process" class and its "fork" permission have prescribed meanings in an
/// SELinux system, so the respective [`Class`] and [`Permission`] are indexed by this
/// constructor. This operation fails if any well-known names are not found in `parsed_policy`.
pub fn new(parsed_policy: ParsedPolicy<PS>) -> Result<Self, anyhow::Error> {
let policy_classes = parsed_policy.classes();
let common_symbols = parsed_policy.common_symbols();
// Accumulate classes indexed by `selinux_common::ObjectClass`. If a class cannot be found
// by name, add it to `missed_classes` for thorough error reporting.
let mut classes = HashMap::new();
let mut missed_classes = vec![];
for known_class in sc::ObjectClass::all_variants().into_iter() {
match get_class_index_by_name(policy_classes, known_class.name()) {
Some(class_index) => {
classes.insert(known_class, class_index);
}
None => {
missed_classes.push(known_class);
}
}
}
if missed_classes.len() > 0 {
return Err(anyhow::anyhow!(
"failed to locate well-known SELinux object classes {:?} in SELinux binary policy",
missed_classes.iter().map(sc::ObjectClass::name).collect::<Vec<_>>()
));
}
// Accumulate permissions indexed by `selinux_common::Permission`. If a permission cannot be
// found by name, add it to `missed_permissions` for thorough error reporting.
let mut permissions = HashMap::new();
let mut missed_permissions = vec![];
for known_permission in sc::Permission::all_variants().into_iter() {
let object_class = known_permission.class();
if let Some(class_index) = classes.get(&object_class) {
let class = &policy_classes[*class_index];
if let Some(permission_index) =
get_permission_index_by_name(common_symbols, class, known_permission.name())
{
permissions.insert(known_permission, permission_index);
} else {
missed_permissions.push(known_permission);
}
} else {
missed_permissions.push(known_permission);
}
}
if missed_permissions.len() > 0 {
return Err(anyhow::anyhow!(
"failed to locate well-known SELinux object permissions {:?} in SELinux binary policy",
missed_permissions
.iter()
.map(|permission| {
let object_class = permission.class();
(object_class.name(), permission.name())
})
.collect::<Vec<_>>()
));
}
// Locate the "object_r" role.
let cached_object_r_role = parsed_policy
.role_by_name("object_r")
.ok_or(anyhow::anyhow!(
"failed to locate well-known 'object_r' role in SELinux binary policy"
))?
.id();
Ok(Self { classes, permissions, parsed_policy, cached_object_r_role })
}
pub fn class<'a>(&'a self, object_class: &sc::ObjectClass) -> &'a Class<PS> {
let class_offset =
*self.classes.get(object_class).expect("policy class index is exhaustive");
&self.parsed_policy.classes()[class_offset]
}
pub fn permission<'a>(&'a self, permission: &sc::Permission) -> &'a Permission<PS> {
let target_class = self.class(&permission.class());
match *self.permissions.get(permission).expect("policy permission index is exhaustive") {
PermissionIndex::Class { permission_index } => {
&target_class.permissions()[permission_index]
}
PermissionIndex::Common { common_symbol_index, permission_index } => {
let common_symbol = &self.parsed_policy().common_symbols()[common_symbol_index];
&common_symbol.permissions()[permission_index]
}
}
}
pub fn new_file_security_context(
&self,
source: &SecurityContext,
target: &SecurityContext,
class: &sc::FileClass,
) -> Result<SecurityContext, NewSecurityContextError> {
let object_class = sc::ObjectClass::from(class.clone());
self.new_security_context(
source,
target,
&object_class,
// The SELinux notebook states the role component defaults to the object_r role.
self.cached_object_r_role,
// The SELinux notebook states the type component defaults to the type of the parent
// directory.
target.type_(),
// The SELinux notebook states the range/level component defaults to the low/current
// level of the creating process.
source.low_level(),
None,
)
}
/// Calculates a new security context, as follows:
///
/// - user: the `source` user, unless the policy contains a default_user statement for `class`.
/// - role:
/// - if the policy contains a role_transition from the `source` role to the `target` type,
/// use the transition role
/// - otherwise, if the policy contains a default_role for `class`, use that default role
/// - lastly, if the policy does not contain either, use `default_role`.
/// - type:
/// - if the policy contains a type_transition from the `source` type to the `target` type,
/// use the transition type
/// - otherwise, if the policy contains a default_type for `class`, use that default type
/// - lastly, if the policy does not contain either, use `default_type`.
/// - range
/// - if the policy contains a range_transition from the `source` type to the `target` type,
/// use the transition range
/// - otherwise, if the policy contains a default_range for `class`, use that default range
/// - lastly, if the policy does not contain either, use the `default_low_level` -
/// `default_high_level` range.
pub fn new_security_context(
&self,
source: &SecurityContext,
target: &SecurityContext,
class: &sc::ObjectClass,
default_role: RoleId,
default_type: TypeId,
default_low_level: &SecurityLevel,
default_high_level: Option<&SecurityLevel>,
) -> Result<SecurityContext, NewSecurityContextError> {
let policy_class = self.class(&class);
let class_defaults = policy_class.defaults();
let user = match class_defaults.user() {
ClassDefault::Source => source.user(),
ClassDefault::Target => target.user(),
_ => source.user(),
};
let role = match self.role_transition_new_role(source.role(), target.type_(), policy_class)
{
Some(new_role) => new_role,
None => match class_defaults.role() {
ClassDefault::Source => source.role(),
ClassDefault::Target => target.role(),
_ => default_role,
},
};
let type_ =
match self.type_transition_new_type(source.type_(), target.type_(), policy_class) {
Some(new_type) => new_type,
None => match class_defaults.type_() {
ClassDefault::Source => source.type_(),
ClassDefault::Target => target.type_(),
_ => default_type,
},
};
let (low_level, high_level) =
match self.range_transition_new_range(source.type_(), target.type_(), policy_class) {
Some((low_level, high_level)) => (low_level, high_level),
None => match class_defaults.range() {
ClassDefaultRange::SourceLow => (source.low_level().clone(), None),
ClassDefaultRange::SourceHigh => {
(source.high_level().unwrap_or(source.low_level()).clone(), None)
}
ClassDefaultRange::SourceLowHigh => {
(source.low_level().clone(), source.high_level().map(Clone::clone))
}
ClassDefaultRange::TargetLow => (target.low_level().clone(), None),
ClassDefaultRange::TargetHigh => {
(target.high_level().unwrap_or(target.low_level()).clone(), None)
}
ClassDefaultRange::TargetLowHigh => {
(target.low_level().clone(), target.high_level().map(Clone::clone))
}
_ => (default_low_level.clone(), default_high_level.map(Clone::clone)),
},
};
// TODO(b/319232900): Ensure that the generated Context has e.g. valid security range.
SecurityContext::new(self, user, role, type_, low_level.clone(), high_level.clone())
.map_err(|err| NewSecurityContextError::MalformedComputedSecurityContext {
source_security_context: source.clone(),
target_security_context: target.clone(),
computed_user: user,
computed_role: role,
computed_type: type_,
computed_low_level: low_level,
computed_high_level: high_level,
error: err,
})
}
pub(crate) fn parsed_policy(&self) -> &ParsedPolicy<PS> {
&self.parsed_policy
}
/// Helper used by `initial_context()` to create a [`sc::SecurityLevel`] instance from
/// the policy fields.
pub(crate) fn security_level(&self, level: &MlsLevel<PS>) -> security_context::SecurityLevel {
security_context::SecurityLevel::new(
level.sensitivity(),
level.categories().spans().map(|span| self.security_context_category(span)).collect(),
)
}
/// Helper used by `security_level()` to create a `Category` instance from policy fields.
fn security_context_category(&self, span: ExtensibleBitmapSpan) -> security_context::Category {
// Spans describe zero-based bit indexes, corresponding to 1-based category Ids.
if span.low == span.high {
security_context::Category::Single(CategoryId(NonZeroU32::new(span.low + 1).unwrap()))
} else {
security_context::Category::Range {
low: CategoryId(NonZeroU32::new(span.low + 1).unwrap()),
high: CategoryId(NonZeroU32::new(span.high + 1).unwrap()),
}
}
}
fn role_transition_new_role(
&self,
current_role: RoleId,
type_: TypeId,
class: &Class<PS>,
) -> Option<RoleId> {
self.parsed_policy
.role_transitions()
.iter()
.find(|role_transition| {
role_transition.current_role() == current_role
&& role_transition.type_() == type_
&& role_transition.class() == class.id()
})
.map(|x| x.new_role())
}
#[allow(dead_code)]
// TODO(http://b/334968228): fn to be used again when checking role allow rules separately from
// SID calculation.
fn role_transition_is_explicitly_allowed(&self, source_role: RoleId, new_role: RoleId) -> bool {
self.parsed_policy
.role_allowlist()
.iter()
.find(|role_allow| {
role_allow.source_role() == source_role && role_allow.new_role() == new_role
})
.is_some()
}
fn type_transition_new_type(
&self,
source_type: TypeId,
target_type: TypeId,
class: &Class<PS>,
) -> Option<TypeId> {
// Return first match. The `checkpolicy` tool will not compile a policy that has
// multiple matches, so behavior on multiple matches is undefined.
self.parsed_policy
.access_vectors()
.iter()
.find(|access_vector| {
access_vector.is_type_transition()
&& access_vector.source_type() == source_type
&& access_vector.target_type() == target_type
&& access_vector.target_class() == class.id()
})
.map(|x| x.new_type().unwrap())
}
fn range_transition_new_range(
&self,
source_type: TypeId,
target_type: TypeId,
class: &Class<PS>,
) -> Option<(security_context::SecurityLevel, Option<security_context::SecurityLevel>)> {
for range_transition in self.parsed_policy.range_transitions() {
if range_transition.source_type() == source_type
&& range_transition.target_type() == target_type
&& range_transition.target_class() == class.id()
{
let mls_range = range_transition.mls_range();
let low_level = self.security_level(mls_range.low());
let high_level =
mls_range.high().as_ref().map(|high_level| self.security_level(high_level));
return Some((low_level, high_level));
}
}
None
}
}
/// Permissions may be stored in their associated [`Class`], or on the class's associated
/// [`CommonSymbol`]. This is a consequence of a limited form of inheritance supported for SELinux
/// policy classes. Classes may inherit from zero or one `common`. For example:
///
/// ```config
/// common file { ioctl read write create [...] }
/// class file inherits file { execute_no_trans entrypoint }
/// ```
///
/// In the above example, the "ioctl" permission for the "file" `class` is stored as a permission
/// on the "file" `common`, whereas the permission "execute_no_trans" is stored as a permission on
/// the "file" `class`.
#[derive(Debug)]
enum PermissionIndex {
/// Permission is located at `Class::permissions()[permission_index]`.
Class { permission_index: usize },
/// Permission is located at
/// `ParsedPolicy::common_symbols()[common_symbol_index].permissions()[permission_index]`.
Common { common_symbol_index: usize, permission_index: usize },
}
fn get_class_index_by_name<'a, PS: ParseStrategy>(
classes: &'a Classes<PS>,
name: &str,
) -> Option<usize> {
let name_bytes = name.as_bytes();
for i in 0..classes.len() {
if classes[i].name_bytes() == name_bytes {
return Some(i);
}
}
None
}
fn get_common_symbol_index_by_name_bytes<'a, PS: ParseStrategy>(
common_symbols: &'a CommonSymbols<PS>,
name_bytes: &[u8],
) -> Option<usize> {
for i in 0..common_symbols.len() {
if common_symbols[i].name_bytes() == name_bytes {
return Some(i);
}
}
None
}
fn get_permission_index_by_name<'a, PS: ParseStrategy>(
common_symbols: &'a CommonSymbols<PS>,
class: &'a Class<PS>,
name: &str,
) -> Option<PermissionIndex> {
if let Some(permission_index) = get_class_permission_index_by_name(class, name) {
Some(PermissionIndex::Class { permission_index })
} else if let Some(common_symbol_index) =
get_common_symbol_index_by_name_bytes(common_symbols, class.common_name_bytes())
{
let common_symbol = &common_symbols[common_symbol_index];
if let Some(permission_index) = get_common_permission_index_by_name(common_symbol, name) {
Some(PermissionIndex::Common { common_symbol_index, permission_index })
} else {
None
}
} else {
None
}
}
fn get_class_permission_index_by_name<'a, PS: ParseStrategy>(
class: &'a Class<PS>,
name: &str,
) -> Option<usize> {
let name_bytes = name.as_bytes();
let permissions = class.permissions();
for i in 0..permissions.len() {
if permissions[i].name_bytes() == name_bytes {
return Some(i);
}
}
None
}
fn get_common_permission_index_by_name<'a, PS: ParseStrategy>(
common_symbol: &'a CommonSymbol<PS>,
name: &str,
) -> Option<usize> {
let name_bytes = name.as_bytes();
let permissions = common_symbol.permissions();
for i in 0..permissions.len() {
if permissions[i].name_bytes() == name_bytes {
return Some(i);
}
}
None
}