blob: 932eb4970dcd4e347f86d55c270e5f3251a1faa8 [file] [log] [blame]
// Copyright 2021 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.
#![recursion_limit = "512"]
mod seq_lock;
use fuchsia_inspect_contrib::profile_duration;
use starnix_sync::LockEqualOrBefore;
use seq_lock::SeqLock;
use selinux::policy::{AccessDecision, AccessVector, SUPPORTED_POLICY_VERSION};
use selinux::{
ClassId, InitialSid, SeLinuxStatus, SeLinuxStatusPublisher, SecurityId, SecurityPermission,
SecurityServer,
};
use starnix_core::device::mem::DevNull;
use starnix_core::mm::memory::MemoryObject;
use starnix_core::security;
use starnix_core::task::{CurrentTask, Kernel};
use starnix_core::vfs::buffers::{InputBuffer, OutputBuffer};
use starnix_core::vfs::pseudo::simple_directory::{SimpleDirectory, SimpleDirectoryMutator};
use starnix_core::vfs::pseudo::simple_file::{
BytesFile, BytesFileOps, SimpleFileNode, parse_unsigned_file,
};
use starnix_core::vfs::pseudo::vec_directory::{VecDirectory, VecDirectoryEntry};
use starnix_core::vfs::{
CacheMode, DirEntry, DirectoryEntryType, DirentSink, FileObject, FileOps, FileSystem,
FileSystemHandle, FileSystemOps, FileSystemOptions, FsNode, FsNodeHandle, FsNodeInfo,
FsNodeOps, FsStr, FsString, MemoryRegularNode, NamespaceNode, emit_dotdot,
fileops_impl_directory, fileops_impl_noop_sync, fileops_impl_seekable,
fileops_impl_unbounded_seek, fs_node_impl_dir_readonly, fs_node_impl_not_dir,
};
use starnix_logging::{
__track_stub_inner, BugRef, impossible_error, log_error, log_info, track_stub,
};
use starnix_sync::{FileOpsCore, Locked, Mutex, Unlocked};
use starnix_types::vfs::default_statfs;
use starnix_uapi::auth::FsCred;
use starnix_uapi::device_type::DeviceType;
use starnix_uapi::errors::Errno;
use starnix_uapi::file_mode::mode;
use starnix_uapi::open_flags::OpenFlags;
use starnix_uapi::{AUDIT_AVC, SELINUX_MAGIC, errno, error, statfs};
use std::borrow::Cow;
use std::num::NonZeroU32;
use std::ops::Deref;
use std::str::FromStr;
use std::sync::{Arc, OnceLock, Weak};
use zerocopy::{Immutable, IntoBytes};
use zx::{self as zx, HandleBased as _};
/// The version of the SELinux "status" file this implementation implements.
const SELINUX_STATUS_VERSION: u32 = 1;
/// Header of the C-style struct exposed via the /sys/fs/selinux/status file,
/// to userspace. Defined here (instead of imported through bindgen) as selinux
/// headers are not exposed through kernel uapi headers.
#[derive(IntoBytes, Copy, Clone, Immutable)]
#[repr(C, align(4))]
struct SeLinuxStatusHeader {
/// Version number of this structure (1).
version: u32,
}
impl Default for SeLinuxStatusHeader {
fn default() -> Self {
Self { version: SELINUX_STATUS_VERSION }
}
}
/// Value part of the C-style struct exposed via the /sys/fs/selinux/status file,
/// to userspace. Defined here (instead of imported through bindgen) as selinux
/// headers are not exposed through kernel uapi headers.
#[derive(IntoBytes, Copy, Clone, Default, Immutable)]
#[repr(C, align(4))]
struct SeLinuxStatusValue {
/// `0` means permissive mode, `1` means enforcing mode.
enforcing: u32,
/// The number of times the selinux policy has been reloaded.
policyload: u32,
/// `0` means allow and `1` means deny unknown object classes/permissions.
deny_unknown: u32,
}
type StatusSeqLock = SeqLock<SeLinuxStatusHeader, SeLinuxStatusValue>;
impl SeLinuxStatusPublisher for StatusSeqLock {
fn set_status(&mut self, policy_status: SeLinuxStatus) {
self.set_value(SeLinuxStatusValue {
enforcing: policy_status.is_enforcing as u32,
policyload: policy_status.change_count,
deny_unknown: policy_status.deny_unknown as u32,
})
}
}
struct SeLinuxFs;
impl FileSystemOps for SeLinuxFs {
fn statfs(
&self,
_locked: &mut Locked<FileOpsCore>,
_fs: &FileSystem,
_current_task: &CurrentTask,
) -> Result<statfs, Errno> {
Ok(default_statfs(SELINUX_MAGIC))
}
fn name(&self) -> &'static FsStr {
"selinuxfs".into()
}
}
/// Implements the /sys/fs/selinux filesystem, as documented in the SELinux
/// Notebook at
/// https://github.com/SELinuxProject/selinux-notebook/blob/main/src/lsm_selinux.md#selinux-filesystem
impl SeLinuxFs {
#[allow(clippy::unwrap_in_result, reason = "Force clippy rule in Starnix")]
fn new_fs<L>(
locked: &mut Locked<L>,
current_task: &CurrentTask,
options: FileSystemOptions,
) -> Result<FileSystemHandle, Errno>
where
L: LockEqualOrBefore<FileOpsCore>,
{
// If SELinux is not enabled then the "selinuxfs" file system does not exist.
let security_server = security::selinuxfs_get_admin_api(current_task)
.ok_or_else(|| errno!(ENODEV, "selinuxfs"))?;
let kernel = current_task.kernel();
let fs = FileSystem::new(locked, kernel, CacheMode::Permanent, SeLinuxFs, options)?;
let root = SimpleDirectory::new();
fs.create_root(fs.allocate_ino(), root.clone());
let dir = SimpleDirectoryMutator::new(fs.clone(), root);
// Read-only files & directories, exposing SELinux internal state.
dir.subdir("avc", 0o555, |dir| {
dir.entry(
"cache_stats",
AvcCacheStatsFile::new_node(security_server.clone()),
mode!(IFREG, 0o444),
);
});
dir.entry("checkreqprot", CheckReqProtApi::new_node(), mode!(IFREG, 0o644));
dir.entry("class", ClassDirectory::new(security_server.clone()), mode!(IFDIR, 0o555));
dir.entry(
"deny_unknown",
DenyUnknownFile::new_node(security_server.clone()),
mode!(IFREG, 0o444),
);
dir.entry(
"reject_unknown",
RejectUnknownFile::new_node(security_server.clone()),
mode!(IFREG, 0o444),
);
dir.subdir("initial_contexts", 0o555, |dir| {
for initial_sid in InitialSid::all_variants().into_iter() {
dir.entry(
initial_sid.name(),
InitialContextFile::new_node(security_server.clone(), *initial_sid),
mode!(IFREG, 0o444),
);
}
});
dir.entry("mls", BytesFile::new_node(b"1".to_vec()), mode!(IFREG, 0o444));
dir.entry("policy", PolicyFile::new_node(security_server.clone()), mode!(IFREG, 0o600));
dir.entry(
"policyvers",
BytesFile::new_node(format!("{}", SUPPORTED_POLICY_VERSION).into_bytes()),
mode!(IFREG, 0o444),
);
// The status file needs to be mmap-able, so use a VMO-backed file. When the selinux state
// changes in the future, the way to update this data (and communicate updates with
// userspace) is to use the ["seqlock"](https://en.wikipedia.org/wiki/Seqlock) technique.
let status_holder = StatusSeqLock::new_default().expect("selinuxfs status seqlock");
let status_file = status_holder
.get_readonly_vmo()
.duplicate_handle(zx::Rights::SAME_RIGHTS)
.map_err(impossible_error)?;
dir.entry(
"status",
MemoryRegularNode::from_memory(Arc::new(MemoryObject::from(status_file))),
mode!(IFREG, 0o444),
);
security_server.set_status_publisher(Box::new(status_holder));
// Write-only files used to configure and query SELinux.
dir.entry(
"access",
AccessApi::new_node(security_server.clone(), current_task.kernel()),
mode!(IFREG, 0o666),
);
dir.entry("context", ContextApi::new_node(security_server.clone()), mode!(IFREG, 0o666));
dir.entry("create", CreateApi::new_node(security_server.clone()), mode!(IFREG, 0o666));
dir.entry("member", MemberApi::new_node(), mode!(IFREG, 0o666));
dir.entry("relabel", RelabelApi::new_node(), mode!(IFREG, 0o666));
dir.entry("user", UserApi::new_node(), mode!(IFREG, 0o666));
dir.entry("load", LoadApi::new_node(security_server.clone()), mode!(IFREG, 0o600));
dir.entry(
"commit_pending_bools",
CommitBooleansApi::new_node(security_server.clone()),
mode!(IFREG, 0o200),
);
// Read/write files allowing values to be queried or changed.
dir.entry("booleans", BooleansDirectory::new(security_server.clone()), mode!(IFDIR, 0o555));
// TODO(b/297313229): Get mode from the container.
dir.entry("enforce", EnforceApi::new_node(security_server), mode!(IFREG, 0o644));
// "/dev/null" equivalent used for file descriptors redirected by SELinux.
let null_ops: Box<dyn FsNodeOps> = (NullFileNode).into();
let mut info = FsNodeInfo::new(mode!(IFCHR, 0o666), FsCred::root());
info.rdev = DeviceType::NULL;
let null_fs_node = fs.create_node_and_allocate_node_id(null_ops, info);
dir.node("null".into(), null_fs_node.clone());
// Initialize selinux kernel state to store a copy of "/sys/fs/selinux/null" for use in
// hooks that redirect file descriptors to null. This has the side-effect of applying the
// policy-defined "devnull" SID to the `null_fs_node`.
let null_ops: Box<dyn FileOps> = Box::new(DevNull);
let null_flags = OpenFlags::empty();
let null_name =
NamespaceNode::new_anonymous(DirEntry::new(null_fs_node, None, "null".into()));
let null_file_object = FileObject::new(current_task, null_ops, null_name, null_flags)
.expect("create file object for just-created selinuxfs/null");
security::selinuxfs_init_null(current_task, &null_file_object);
Ok(fs)
}
}
/// "load" API, accepting a binary policy in a single `write()` operation, which must be at seek
/// position zero.
struct LoadApi {
security_server: Arc<SecurityServer>,
}
impl LoadApi {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
SeLinuxApi::new_node(move || Ok(Self { security_server: security_server.clone() }))
}
}
impl SeLinuxApiOps for LoadApi {
fn api_write_permission() -> SecurityPermission {
SecurityPermission::LoadPolicy
}
fn api_write_with_task(
&self,
locked: &mut Locked<FileOpsCore>,
current_task: &CurrentTask,
data: Vec<u8>,
) -> Result<(), Errno> {
profile_duration!("selinuxfs.load");
log_info!("Loading {} byte policy", data.len());
self.security_server.load_policy(data).map_err(|error| {
log_error!("Policy load error: {}", error);
errno!(EINVAL)
})?;
// Allow one-time initialization of state that requires a loaded policy.
security::selinuxfs_policy_loaded(locked, current_task);
Ok(())
}
}
/// "policy" file, which allows the currently-loaded binary policy, to be read as a normal file,
/// including supporting seek-aware reads.
struct PolicyFile {
security_server: Arc<SecurityServer>,
}
impl PolicyFile {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
SimpleFileNode::new(move || Ok(Self { security_server: security_server.clone() }))
}
}
impl FileOps for PolicyFile {
fileops_impl_seekable!();
fileops_impl_noop_sync!();
fn read(
&self,
_locked: &mut Locked<FileOpsCore>,
_file: &FileObject,
_current_task: &CurrentTask,
offset: usize,
data: &mut dyn OutputBuffer,
) -> Result<usize, Errno> {
let policy = self.security_server.get_binary_policy().ok_or_else(|| errno!(EINVAL))?;
let policy_bytes: &[u8] = policy.deref();
if offset >= policy_bytes.len() {
return Ok(0);
}
data.write(&policy_bytes[offset..])
}
fn write(
&self,
_locked: &mut Locked<FileOpsCore>,
_file: &FileObject,
_current_task: &CurrentTask,
_offset: usize,
_data: &mut dyn InputBuffer,
) -> Result<usize, Errno> {
error!(EACCES)
}
}
/// "enforce" API used to control whether SELinux is globally permissive, versus enforcing.
struct EnforceApi {
security_server: Arc<SecurityServer>,
}
impl EnforceApi {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
SeLinuxApi::new_node(move || Ok(Self { security_server: security_server.clone() }))
}
}
impl SeLinuxApiOps for EnforceApi {
fn api_write_permission() -> SecurityPermission {
SecurityPermission::SetEnforce
}
fn api_write(&self, data: Vec<u8>) -> Result<(), Errno> {
// Callers may write any number of times to this API, so long as the `data` is valid.
profile_duration!("selinuxfs.enforce");
let enforce = parse_unsigned_file::<u32>(&data)? != 0;
self.security_server.set_enforcing(enforce);
Ok(())
}
fn api_read(&self) -> Result<Cow<'_, [u8]>, Errno> {
Ok(self.security_server.is_enforcing().then_some(b"1").unwrap_or(b"0").into())
}
}
/// "deny_unknown" file which exposes how classes & permissions not defined by the policy should
/// be allowed or denied.
struct DenyUnknownFile {
security_server: Arc<SecurityServer>,
}
impl DenyUnknownFile {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
BytesFile::new_node(Self { security_server })
}
}
impl BytesFileOps for DenyUnknownFile {
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
Ok(format!("{}", self.security_server.deny_unknown() as u32).into_bytes().into())
}
}
/// "reject_unknown" file which exposes whether kernel classes & permissions not defined by the
/// policy would have prevented the policy being loaded.
struct RejectUnknownFile {
security_server: Arc<SecurityServer>,
}
impl RejectUnknownFile {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
BytesFile::new_node(Self { security_server })
}
}
impl BytesFileOps for RejectUnknownFile {
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
Ok(format!("{}", self.security_server.reject_unknown() as u32).into_bytes().into())
}
}
/// "create" API used to determine the Security Context to associate with a new resource instance
/// based on source, target, and target class.
struct CreateApi {
security_server: Arc<SecurityServer>,
result: OnceLock<SecurityId>,
}
impl CreateApi {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
SeLinuxApi::new_node(move || {
Ok(Self { security_server: security_server.clone(), result: OnceLock::new() })
})
}
}
impl SeLinuxApiOps for CreateApi {
fn api_write_permission() -> SecurityPermission {
SecurityPermission::ComputeCreate
}
fn api_write(&self, data: Vec<u8>) -> Result<(), Errno> {
if self.result.get().is_some() {
// The "create" API can be written-to at most once.
return error!(EBUSY);
}
let data = str::from_utf8(&data).map_err(|_| errno!(EINVAL))?;
// Requests consist of three mandatory space-separated elements.
let mut parts = data.split_whitespace();
// <scontext>: describes the subject that is creating the new object.
let scontext = parts.next().ok_or_else(|| errno!(EINVAL))?;
let scontext = self
.security_server
.security_context_to_sid(scontext.into())
.map_err(|_| errno!(EINVAL))?;
// <tcontext>: describes the target (e.g. parent directory) of the create operation.
let tcontext = parts.next().ok_or_else(|| errno!(EINVAL))?;
let tcontext = self
.security_server
.security_context_to_sid(tcontext.into())
.map_err(|_| errno!(EINVAL))?;
// <tclass>: the policy-specific Id of the created object's class, as a decimal integer.
// Class Ids are obtained via lookups in the SELinuxFS "class" directory.
let tclass = parts.next().ok_or_else(|| errno!(EINVAL))?;
let tclass = u32::from_str(tclass).map_err(|_| errno!(EINVAL))?;
let tclass = ClassId::new(NonZeroU32::new(tclass).ok_or_else(|| errno!(EINVAL))?);
// Optional <name>: the final element of the path of the newly-created object. This allows
// filename-dependent transition rules to be applied to the computation.
let tname = parts.next();
if tname.is_some() {
track_stub!(TODO("https://fxbug.dev/361552580"), "selinux create with name");
return error!(ENOTSUP);
}
// There must be no further trailing arguments.
if parts.next().is_some() {
return error!(EINVAL);
}
let result = self
.security_server
.compute_create_sid(scontext, tcontext, tclass)
.map_err(|_| errno!(EINVAL))?;
self.result.set(result).map_err(|_| errno!(EINVAL))?;
Ok(())
}
#[allow(clippy::unwrap_in_result, reason = "Force clippy rule in Starnix")]
fn api_read(&self) -> Result<Cow<'_, [u8]>, Errno> {
let maybe_context = self
.result
.get()
.map(|sid| self.security_server.sid_to_security_context(*sid).unwrap());
let context = maybe_context.unwrap_or_else(|| Vec::new());
Ok(context.into())
}
}
/// "member" API used to determine the Security Context to associate with a new resource instance
/// based on source, target, and target class and `type_member` rules.
struct MemberApi;
impl MemberApi {
fn new_node() -> impl FsNodeOps {
SeLinuxApi::new_node(|| Ok(Self {}))
}
}
impl SeLinuxApiOps for MemberApi {
fn api_write_permission() -> SecurityPermission {
SecurityPermission::ComputeMember
}
fn api_write(&self, _data: Vec<u8>) -> Result<(), Errno> {
track_stub!(TODO("https://fxbug.dev/399069170"), "selinux member");
error!(ENOTSUP)
}
fn api_read(&self) -> Result<Cow<'_, [u8]>, Errno> {
error!(ENOTSUP)
}
}
/// "relabel" API used to determine the Security Context to associate with a new resource instance
/// based on source, target, and target class and `type_change` rules.
struct RelabelApi;
impl RelabelApi {
fn new_node() -> impl FsNodeOps {
SeLinuxApi::new_node(|| Ok(Self {}))
}
}
impl SeLinuxApiOps for RelabelApi {
fn api_write_permission() -> SecurityPermission {
SecurityPermission::ComputeRelabel
}
fn api_write(&self, _data: Vec<u8>) -> Result<(), Errno> {
track_stub!(TODO("https://fxbug.dev/399069766"), "selinux relabel");
error!(ENOTSUP)
}
fn api_read(&self) -> Result<Cow<'_, [u8]>, Errno> {
error!(ENOTSUP)
}
}
/// "user" API used to perform a user decision.
struct UserApi;
impl UserApi {
fn new_node() -> impl FsNodeOps {
SeLinuxApi::new_node(|| Ok(Self {}))
}
}
impl SeLinuxApiOps for UserApi {
fn api_write_permission() -> SecurityPermission {
SecurityPermission::ComputeUser
}
fn api_write(&self, _data: Vec<u8>) -> Result<(), Errno> {
track_stub!(TODO("https://fxbug.dev/411433214"), "selinux user");
error!(ENOTSUP)
}
fn api_read(&self) -> Result<Cow<'_, [u8]>, Errno> {
error!(ENOTSUP)
}
}
struct CheckReqProtApi;
impl CheckReqProtApi {
fn new_node() -> impl FsNodeOps {
SeLinuxApi::new_node(|| Ok(Self {}))
}
}
impl SeLinuxApiOps for CheckReqProtApi {
fn api_write_permission() -> SecurityPermission {
SecurityPermission::SetCheckReqProt
}
fn api_write(&self, data: Vec<u8>) -> Result<(), Errno> {
let _checkreqprot = parse_unsigned_file::<u32>(&data)? != 0;
track_stub!(TODO("https://fxbug.dev/322874766"), "selinux checkreqprot");
Ok(())
}
}
/// "context" API which accepts a Security Context in a single `write()` operation, and validates
/// it against the loaded policy. If the Context is invalid then the `write()` returns `EINVAL`,
/// otherwise the Context may be read back from the file.
struct ContextApi {
security_server: Arc<SecurityServer>,
// Holds the SID representing the Security Context that the caller wrote to the file.
context_sid: Mutex<Option<SecurityId>>,
}
impl ContextApi {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
SeLinuxApi::new_node(move || {
Ok(Self { security_server: security_server.clone(), context_sid: Mutex::default() })
})
}
}
impl SeLinuxApiOps for ContextApi {
fn api_write_permission() -> SecurityPermission {
SecurityPermission::CheckContext
}
fn api_write(&self, data: Vec<u8>) -> Result<(), Errno> {
profile_duration!("selinuxfs.context");
// If this instance was already written-to then fail the operation.
let mut context_sid = self.context_sid.lock();
if context_sid.is_some() {
return error!(EBUSY);
}
// Validate that the `data` describe valid user, role, type, etc by attempting to create
// a SID from it.
*context_sid = Some(
self.security_server
.security_context_to_sid(data.as_slice().into())
.map_err(|_| errno!(EINVAL))?,
);
Ok(())
}
fn api_read(&self) -> Result<Cow<'_, [u8]>, Errno> {
// Read returns the Security Context the caller previously wrote to the file, normalized
// as a consequence of the Context->SID->Context round-trip. If no Context had been written
// by the caller, then this API file behaves as though empty.
// TODO: https://fxbug.dev/319629153 - If `write()` failed due to an invalid Context then
// should `read()` also fail, or return an empty result?
let maybe_sid = *self.context_sid.lock();
let result = maybe_sid
.and_then(|sid| self.security_server.sid_to_security_context(sid))
.unwrap_or_default();
Ok(result.into())
}
}
struct InitialContextFile {
security_server: Arc<SecurityServer>,
initial_sid: InitialSid,
}
impl InitialContextFile {
fn new_node(security_server: Arc<SecurityServer>, initial_sid: InitialSid) -> impl FsNodeOps {
BytesFile::new_node(Self { security_server, initial_sid })
}
}
impl BytesFileOps for InitialContextFile {
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
profile_duration!("selinuxfs.initial_sid");
let sid = self.initial_sid.into();
if let Some(context) = self.security_server.sid_to_security_context(sid) {
Ok(context.into())
} else {
// Looking up an initial SID can only fail if no policy is loaded, in
// which case the file contains the name of the initial SID, rather
// than a Security Context value.
Ok(self.initial_sid.name().as_bytes().into())
}
}
}
/// Extends a calculated `AccessDecision` with an additional permission set describing which
/// permissions were actually `decided` - all other permissions in the `AccessDecision` structure
/// should be assumed to be un-`decided`. This allows the "access" API to return partial results, to
/// force userspace to re-query the API if any un-`decided` permission is later requested.
struct AccessDecisionAndDecided {
decision: AccessDecision,
decided: AccessVector,
}
struct AccessApi {
security_server: Arc<SecurityServer>,
result: OnceLock<AccessDecisionAndDecided>,
// Required to support audit-logging of requests granted via `todo_deny` exceptions.
kernel: Weak<Kernel>,
}
impl AccessApi {
fn new_node(security_server: Arc<SecurityServer>, kernel: &Arc<Kernel>) -> impl FsNodeOps {
let kernel = Arc::downgrade(kernel);
SeLinuxApi::new_node(move || {
Ok(Self {
security_server: security_server.clone(),
result: OnceLock::default(),
kernel: kernel.clone(),
})
})
}
}
impl SeLinuxApiOps for AccessApi {
fn api_write_permission() -> SecurityPermission {
SecurityPermission::ComputeAv
}
fn api_write(&self, data: Vec<u8>) -> Result<(), Errno> {
if self.result.get().is_some() {
// The "access" API can be written-to at most once.
return error!(EBUSY);
}
let data = str::from_utf8(&data).map_err(|_| errno!(EINVAL))?;
// Requests consist of three mandatory space-separated elements, and one optional element.
let mut parts = data.split_whitespace();
// <scontext>: describes the subject acting on the class.
let scontext_str = parts.next().ok_or_else(|| errno!(EINVAL))?;
let scontext = self
.security_server
.security_context_to_sid(scontext_str.into())
.map_err(|_| errno!(EINVAL))?;
// <tcontext>: describes the target (e.g. parent directory) of the operation.
let tcontext_str = parts.next().ok_or_else(|| errno!(EINVAL))?;
let tcontext = self
.security_server
.security_context_to_sid(tcontext_str.into())
.map_err(|_| errno!(EINVAL))?;
// <tclass>: the policy-specific Id of the target class, as a decimal integer.
// Class Ids are obtained via lookups in the SELinuxFS "class" directory.
let tclass = parts.next().ok_or_else(|| errno!(EINVAL))?;
let tclass_id = u32::from_str(tclass).map_err(|_| errno!(EINVAL))?;
let tclass = ClassId::new(NonZeroU32::new(tclass_id).ok_or_else(|| errno!(EINVAL))?).into();
// <request>: the set of permissions that the caller requests.
let requested = if let Some(requested) = parts.next() {
AccessVector::from_str(requested).map_err(|_| errno!(EINVAL))?
} else {
AccessVector::ALL
};
// This API does not appear to treat trailing arguments as invalid.
// Perform the access decision calculation.
let permission_check = self.security_server.as_permission_check();
let mut decision = permission_check.compute_access_decision(scontext, tcontext, tclass);
// `compute_access_decision()` returns an `AccessDecision` with results calculated for all
// permissions defined by policy, so by default the "access" API reports all permissions as
// having been `decided`.
let mut decided = AccessVector::ALL;
// If there is a `todo_bug` associated with the decision then grant all permissions and
// make a best-effort attempt to emit a log for missing permissions.
let Some(kernel) = self.kernel.upgrade() else {
return error!(EINVAL);
};
if let Some(todo_bug) = decision.todo_bug {
let denied = AccessVector::ALL - decision.allow;
let audited_denied = denied & decision.auditdeny;
let requested_has_audited_denial = audited_denied & requested != AccessVector::NONE;
if requested_has_audited_denial {
// One or more requested permissions would be denied, and the denial audit-logged,
// so emit a track-stub report and a description of the request and result.
// Leave all permissions `decided`, so that only the first such failure is audited.
__track_stub_inner(
BugRef::from(todo_bug),
"Enforce SELinuxFS access API",
None,
std::panic::Location::caller(),
);
let audit_message = format!(
"avc: todo_deny {{ ACCESS_API }} scontext={scontext_str:?} tcontext={tcontext_str:?} tclass={tclass_id} requested={requested:?}",
);
kernel.audit_logger().audit_log(AUDIT_AVC as u16, || audit_message);
} else {
// All requested permissions were granted. To allow "todo_deny" logs and track-stub
// tracking of permissions that would otherwise be denied & audited, remove those
// permissions from the `decided` set, to signal that the userspace AVC should re-
// query rather than using these cached results.
decided -= audited_denied;
}
// Grant all permissions in the returned result, so that clients that ignore the
// `decided` set will still have the allowance applied to all permissions.
decision.allow = AccessVector::ALL;
}
self.result
.set(AccessDecisionAndDecided { decision, decided })
.map_err(|_| errno!(EINVAL))?;
Ok(())
}
fn api_read(&self) -> Result<Cow<'_, [u8]>, Errno> {
let Some(AccessDecisionAndDecided { decision, decided }) = self.result.get() else {
return Ok(Vec::new().into());
};
let allowed = decision.allow;
let auditallow = decision.auditallow;
let auditdeny = decision.auditdeny;
let flags = decision.flags;
// TODO: https://fxbug.dev/361551536 - `seqno` should reflect the policy revision from
// which the result was calculated, to allow the client to re-try if the policy changed.
const SEQNO: u32 = 1;
// Result format is: allowed decided auditallow auditdeny seqno flags
// Everything but seqno must be in hexadecimal format and represents a bits field.
let result =
format!("{allowed:x} {decided:x} {auditallow:x} {auditdeny:x} {SEQNO} {flags:x}");
Ok(result.into_bytes().into())
}
}
struct NullFileNode;
impl FsNodeOps for NullFileNode {
fs_node_impl_not_dir!();
fn create_file_ops(
&self,
_locked: &mut Locked<FileOpsCore>,
_node: &FsNode,
_current_task: &CurrentTask,
_flags: OpenFlags,
) -> Result<Box<dyn FileOps>, Errno> {
Ok(Box::new(DevNull))
}
}
#[derive(Clone)]
struct BooleansDirectory {
security_server: Arc<SecurityServer>,
}
impl BooleansDirectory {
fn new(security_server: Arc<SecurityServer>) -> Self {
Self { security_server }
}
}
impl FsNodeOps for BooleansDirectory {
fs_node_impl_dir_readonly!();
fn create_file_ops(
&self,
_locked: &mut Locked<FileOpsCore>,
_node: &FsNode,
_current_task: &CurrentTask,
_flags: OpenFlags,
) -> Result<Box<dyn FileOps>, Errno> {
Ok(Box::new(self.clone()))
}
fn lookup(
&self,
_locked: &mut Locked<FileOpsCore>,
node: &FsNode,
current_task: &CurrentTask,
name: &FsStr,
) -> Result<FsNodeHandle, Errno> {
let utf8_name = String::from_utf8(name.to_vec()).map_err(|_| errno!(ENOENT))?;
if self.security_server.conditional_booleans().contains(&utf8_name) {
profile_duration!("selinuxfs.booleans.lookup");
Ok(node.fs().create_node_and_allocate_node_id(
BooleanFile::new_node(self.security_server.clone(), utf8_name),
FsNodeInfo::new(mode!(IFREG, 0o644), current_task.current_fscred()),
))
} else {
error!(ENOENT)
}
}
}
impl FileOps for BooleansDirectory {
fileops_impl_directory!();
fileops_impl_noop_sync!();
fileops_impl_unbounded_seek!();
fn readdir(
&self,
_locked: &mut Locked<FileOpsCore>,
file: &FileObject,
_current_task: &CurrentTask,
sink: &mut dyn DirentSink,
) -> Result<(), Errno> {
profile_duration!("selinuxfs.booleans.readdir");
emit_dotdot(file, sink)?;
// `emit_dotdot()` provides the first two directory entries, so that the entries for
// the conditional booleans start from offset 2.
let iter_offset = sink.offset() - 2;
for name in self.security_server.conditional_booleans().iter().skip(iter_offset as usize) {
sink.add(
file.fs.allocate_ino(),
/* next offset = */ sink.offset() + 1,
DirectoryEntryType::REG,
FsString::from(name.as_bytes()).as_ref(),
)?;
}
Ok(())
}
}
struct BooleanFile {
security_server: Arc<SecurityServer>,
name: String,
}
impl BooleanFile {
fn new_node(security_server: Arc<SecurityServer>, name: String) -> impl FsNodeOps {
BytesFile::new_node(BooleanFile { security_server, name })
}
}
impl BytesFileOps for BooleanFile {
fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
profile_duration!("selinuxfs.boolean.write");
let value = parse_unsigned_file::<u32>(&data)? != 0;
self.security_server.set_pending_boolean(&self.name, value).map_err(|_| errno!(EIO))
}
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
profile_duration!("selinuxfs.boolean.read");
// Each boolean has a current active value, and a pending value that
// will become active if "commit_pending_booleans" is written to.
// e.g. "1 0" will be read if a boolean is True but will become False.
let (active, pending) =
self.security_server.get_boolean(&self.name).map_err(|_| errno!(EIO))?;
Ok(format!("{} {}", active as u32, pending as u32).into_bytes().into())
}
}
struct CommitBooleansApi {
security_server: Arc<SecurityServer>,
}
impl CommitBooleansApi {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
SeLinuxApi::new_node(move || {
Ok(CommitBooleansApi { security_server: security_server.clone() })
})
}
}
impl SeLinuxApiOps for CommitBooleansApi {
fn api_write_permission() -> SecurityPermission {
SecurityPermission::SetBool
}
fn api_write(&self, data: Vec<u8>) -> Result<(), Errno> {
profile_duration!("selinuxfs.commit_booleans.write");
// "commit_pending_booleans" expects a numeric argument, which is
// interpreted as a boolean, with the pending booleans committed if the
// value is true (i.e. non-zero).
let commit = parse_unsigned_file::<u32>(&data)? != 0;
if commit {
self.security_server.commit_pending_booleans();
}
Ok(())
}
}
struct ClassDirectory {
security_server: Arc<SecurityServer>,
}
impl ClassDirectory {
fn new(security_server: Arc<SecurityServer>) -> Self {
Self { security_server }
}
}
impl FsNodeOps for ClassDirectory {
fs_node_impl_dir_readonly!();
/// Returns the set of classes under the "class" directory.
fn create_file_ops(
&self,
_locked: &mut Locked<FileOpsCore>,
_node: &FsNode,
_current_task: &CurrentTask,
_flags: OpenFlags,
) -> Result<Box<dyn FileOps>, Errno> {
Ok(VecDirectory::new_file(
self.security_server
.class_names()
.map_err(|_| errno!(ENOENT))?
.iter()
.map(|class_name| VecDirectoryEntry {
entry_type: DirectoryEntryType::DIR,
name: class_name.clone().into(),
inode: None,
})
.collect(),
))
}
fn lookup(
&self,
_locked: &mut Locked<FileOpsCore>,
node: &FsNode,
_current_task: &CurrentTask,
name: &FsStr,
) -> Result<FsNodeHandle, Errno> {
profile_duration!("selinuxfs.class.lookup");
let id: u32 = self
.security_server
.class_id_by_name(&name.to_string())
.map_err(|_| errno!(EINVAL))?
.into();
let fs = node.fs();
let dir = SimpleDirectory::new();
dir.edit(&fs, |dir| {
let index_bytes = format!("{}", id).into_bytes();
dir.entry("index", BytesFile::new_node(index_bytes), mode!(IFREG, 0o444));
dir.entry(
"perms",
PermsDirectory::new(self.security_server.clone(), name.to_string()),
mode!(IFDIR, 0o555),
);
});
Ok(dir.into_node(&fs, 0o555))
}
}
/// Represents the perms/ directory under each class entry of the SeLinuxClassDirectory.
struct PermsDirectory {
security_server: Arc<SecurityServer>,
class_name: String,
}
impl PermsDirectory {
fn new(security_server: Arc<SecurityServer>, class_name: String) -> Self {
Self { security_server, class_name }
}
}
impl FsNodeOps for PermsDirectory {
fs_node_impl_dir_readonly!();
/// Lists all available permissions for the corresponding class.
fn create_file_ops(
&self,
_locked: &mut Locked<FileOpsCore>,
_node: &FsNode,
_current_task: &CurrentTask,
_flags: OpenFlags,
) -> Result<Box<dyn FileOps>, Errno> {
Ok(VecDirectory::new_file(
self.security_server
.class_permissions_by_name(&self.class_name)
.map_err(|_| errno!(ENOENT))?
.iter()
.map(|(_permission_id, permission_name)| VecDirectoryEntry {
entry_type: DirectoryEntryType::DIR,
name: permission_name.clone().into(),
inode: None,
})
.collect(),
))
}
fn lookup(
&self,
_locked: &mut Locked<FileOpsCore>,
node: &FsNode,
current_task: &CurrentTask,
name: &FsStr,
) -> Result<FsNodeHandle, Errno> {
profile_duration!("selinuxfs.perms.lookup");
let found_permission_id = self
.security_server
.class_permissions_by_name(&(self.class_name))
.map_err(|_| errno!(ENOENT))?
.iter()
.find(|(_permission_id, permission_name)| permission_name == name)
.ok_or_else(|| errno!(ENOENT))?
.0;
Ok(node.fs().create_node_and_allocate_node_id(
BytesFile::new_node(format!("{}", found_permission_id).into_bytes()),
FsNodeInfo::new(mode!(IFREG, 0o444), current_task.current_fscred()),
))
}
}
/// Exposes AVC cache statistics from the SELinux security server to userspace.
struct AvcCacheStatsFile {
security_server: Arc<SecurityServer>,
}
impl AvcCacheStatsFile {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
BytesFile::new_node(Self { security_server })
}
}
impl BytesFileOps for AvcCacheStatsFile {
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
let stats = self.security_server.avc_cache_stats();
Ok(format!(
"lookups hits misses allocations reclaims frees\n{} {} {} {} {} {}\n",
stats.lookups, stats.hits, stats.misses, stats.allocs, stats.reclaims, stats.frees
)
.into_bytes()
.into())
}
}
/// File node implementation tailored to the behaviour of the APIs exposed to userspace via the
/// SELinux filesystem. These API files share some unusual behaviours:
///
/// (1) Seek Position:
/// API files in the SELinux filesystem do have persistent seek offsets, but asymmetric behaviour
/// for read and write operations:
/// - Read operations respect the file offset, and increment it.
/// - Write operations do not increment the file offset. This is important for APIs such as
/// "create", which are used by `write()`ing a query and then `read()`ing the resulting value,
/// since otherwise the `read()` would start from the end of the `write()`.
///
/// API files do not handle non-zero offset `write()`s consistently. Some, (e.g. "context"), ignore
/// the offset, while others (e.g. "load") will fail with `EINVAL` if it is non-zero.
///
/// (2) Single vs Multi-Request:
/// Most API files may be `read()` from any number of times, but only support a single `write()`
/// operation. Attempting to `write()` a second time will return `EBUSY`.
///
/// (3) Error Handling:
/// Once an operation on an API file has failed, all subsequent operations on that file will
/// also fail, with the same error code. e.g. Attempting multiple `write()` operations will
/// return `EBUSY` from the second and subsequent calls, but subsequent calls to `read()`,
/// `seek()` etc will also return `EBUSY`.
///
/// This helper currently implements asymmetric seek behaviour, and permission checks on write
/// operations.
struct SeLinuxApi<T: SeLinuxApiOps + Sync + Send + 'static> {
ops: T,
}
impl<T: SeLinuxApiOps + Sync + Send + 'static> SeLinuxApi<T> {
/// Returns a new `SeLinuxApi` file node that will use `create_ops` to create a new `SeLinuxApiOps`
/// instance every time a caller opens the file.
fn new_node<F>(create_ops: F) -> impl FsNodeOps
where
F: Fn() -> Result<T, Errno> + Send + Sync + 'static,
{
SimpleFileNode::new(move || create_ops().map(|ops| SeLinuxApi { ops }))
}
}
/// Trait implemented for each SELinux API file (e.g. "create", "load") to define its behaviour.
trait SeLinuxApiOps {
/// Returns the "security" class permission that is required in order to write to the API file.
fn api_write_permission() -> SecurityPermission;
/// Returns true if writes ignore the seek offset, rather than requiring it to be zero.
fn api_write_ignores_offset() -> bool {
false
}
/// Processes a request written to an API file.
fn api_write(&self, _data: Vec<u8>) -> Result<(), Errno> {
error!(EINVAL)
}
/// Returns the complete contents of this API file.
fn api_read(&self) -> Result<Cow<'_, [u8]>, Errno> {
error!(EINVAL)
}
/// Variant of `api_write()` that additionally receives the `current_task`.
fn api_write_with_task(
&self,
_locked: &mut Locked<FileOpsCore>,
_current_task: &CurrentTask,
data: Vec<u8>,
) -> Result<(), Errno> {
self.api_write(data)
}
}
impl<T: SeLinuxApiOps + Sync + Send + 'static> FileOps for SeLinuxApi<T> {
fileops_impl_seekable!();
fileops_impl_noop_sync!();
fn writes_update_seek_offset(&self) -> bool {
false
}
fn read(
&self,
_locked: &mut Locked<FileOpsCore>,
_file: &FileObject,
_current_task: &CurrentTask,
offset: usize,
data: &mut dyn OutputBuffer,
) -> Result<usize, Errno> {
profile_duration!("selinuxfs.api.read");
let response = self.ops.api_read()?;
data.write(&response[offset..])
}
fn write(
&self,
locked: &mut Locked<FileOpsCore>,
_file: &FileObject,
current_task: &CurrentTask,
offset: usize,
data: &mut dyn InputBuffer,
) -> Result<usize, Errno> {
profile_duration!("selinuxfs.api.write");
if offset != 0 && !T::api_write_ignores_offset() {
return error!(EINVAL);
}
security::selinuxfs_check_access(current_task, T::api_write_permission())?;
let data = data.read_all()?;
let data_len = data.len();
self.ops.api_write_with_task(locked, current_task, data)?;
Ok(data_len)
}
}
/// Returns the "selinuxfs" file system, used by the system userspace to administer SELinux.
pub fn selinux_fs(
locked: &mut Locked<Unlocked>,
current_task: &CurrentTask,
options: FileSystemOptions,
) -> Result<FileSystemHandle, Errno> {
struct SeLinuxFsHandle(FileSystemHandle);
profile_duration!("selinuxfs.mount");
Ok(current_task
.kernel()
.expando
.get_or_try_init(|| Ok(SeLinuxFsHandle(SeLinuxFs::new_fs(locked, current_task, options)?)))?
.0
.clone())
}
#[cfg(test)]
mod tests {
use super::*;
use selinux::SecurityServer;
use zerocopy::{FromBytes, KnownLayout};
use zx::{self as zx, AsHandleRef as _};
#[fuchsia::test]
fn status_vmo_has_correct_size_and_rights() {
// The current version of the "status" file contains five packed
// u32 values.
const STATUS_T_SIZE: usize = size_of::<u32>() * 5;
let status_holder = StatusSeqLock::new_default().unwrap();
let status_vmo = status_holder.get_readonly_vmo();
// Verify the content and actual size of the structure are as expected.
let content_size = status_vmo.get_content_size().unwrap() as usize;
assert_eq!(content_size, STATUS_T_SIZE);
let actual_size = status_vmo.get_size().unwrap() as usize;
assert!(actual_size >= STATUS_T_SIZE);
// Ensure the returned handle is read-only and non-resizable.
let rights = status_vmo.basic_info().unwrap().rights;
assert_eq!((rights & zx::Rights::MAP), zx::Rights::MAP);
assert_eq!((rights & zx::Rights::READ), zx::Rights::READ);
assert_eq!((rights & zx::Rights::GET_PROPERTY), zx::Rights::GET_PROPERTY);
assert_eq!((rights & zx::Rights::WRITE), zx::Rights::NONE);
assert_eq!((rights & zx::Rights::RESIZE), zx::Rights::NONE);
}
#[derive(KnownLayout, FromBytes)]
#[repr(C, align(4))]
struct TestSeLinuxStatusT {
version: u32,
sequence: u32,
enforcing: u32,
policyload: u32,
deny_unknown: u32,
}
fn with_status_t<R>(
status_vmo: &Arc<zx::Vmo>,
do_test: impl FnOnce(&TestSeLinuxStatusT) -> R,
) -> R {
let flags = zx::VmarFlags::PERM_READ
| zx::VmarFlags::ALLOW_FAULTS
| zx::VmarFlags::REQUIRE_NON_RESIZABLE;
let map_addr = fuchsia_runtime::vmar_root_self()
.map(0, status_vmo, 0, size_of::<TestSeLinuxStatusT>(), flags)
.unwrap();
#[allow(
clippy::undocumented_unsafe_blocks,
reason = "Force documented unsafe blocks in Starnix"
)]
let mapped_status = unsafe { &mut *(map_addr as *mut TestSeLinuxStatusT) };
let result = do_test(mapped_status);
#[allow(
clippy::undocumented_unsafe_blocks,
reason = "Force documented unsafe blocks in Starnix"
)]
unsafe {
fuchsia_runtime::vmar_root_self()
.unmap(map_addr, size_of::<TestSeLinuxStatusT>())
.unwrap()
};
result
}
#[fuchsia::test]
fn status_file_layout() {
let security_server = SecurityServer::new_default();
let status_holder = StatusSeqLock::new_default().unwrap();
let status_vmo = status_holder.get_readonly_vmo();
security_server.set_status_publisher(Box::new(status_holder));
security_server.set_enforcing(false);
let mut seq_no: u32 = 0;
with_status_t(&status_vmo, |status| {
assert_eq!(status.version, SELINUX_STATUS_VERSION);
assert_eq!(status.enforcing, 0);
seq_no = status.sequence;
assert_eq!(seq_no % 2, 0);
});
security_server.set_enforcing(true);
with_status_t(&status_vmo, |status| {
assert_eq!(status.version, SELINUX_STATUS_VERSION);
assert_eq!(status.enforcing, 1);
assert_ne!(status.sequence, seq_no);
seq_no = status.sequence;
assert_eq!(seq_no % 2, 0);
});
}
#[fuchsia::test]
fn status_accurate_directly_following_set_status_publisher() {
let security_server = SecurityServer::new_default();
let status_holder = StatusSeqLock::new_default().unwrap();
let status_vmo = status_holder.get_readonly_vmo();
// Ensure a change in status-visible security server state is made before invoking
// `set_status_publisher()`.
assert_eq!(false, security_server.is_enforcing());
security_server.set_enforcing(true);
security_server.set_status_publisher(Box::new(status_holder));
with_status_t(&status_vmo, |status| {
// Ensure latest `enforcing` state is reported immediately following
// `set_status_publisher()`.
assert_eq!(status.enforcing, 1);
});
}
}