blob: 1f0a4740d200c9b536725799c0f7068d15c8e78b [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.
use crate::{
task::{CurrentTask, Task},
vfs::{
buffers::{InputBuffer, OutputBuffer},
emit_dotdot, fileops_impl_directory, fileops_impl_nonseekable, fs_node_impl_dir_readonly,
fs_node_impl_not_dir, parse_unsigned_file, unbounded_seek, BytesFile, BytesFileOps,
CacheMode, DirectoryEntryType, DirentSink, FileObject, FileOps, FileSystem,
FileSystemHandle, FileSystemOps, FileSystemOptions, FsNode, FsNodeHandle, FsNodeInfo,
FsNodeOps, FsStr, FsString, SeekTarget, StaticDirectoryBuilder, VecDirectory,
VecDirectoryEntry, VmoFileNode,
},
};
use bstr::ByteSlice;
use selinux::{
security_server::{SecurityServer, SecurityServerStatus as _},
InitialSid, SecurityId,
};
use selinux_policy::SUPPORTED_POLICY_VERSION;
use starnix_logging::{log_error, log_info, track_stub};
use starnix_sync::{FileOpsCore, Locked, Mutex, WriteOps};
use starnix_uapi::{
device_type::DeviceType,
errno, error,
errors::Errno,
file_mode::mode,
off_t,
open_flags::OpenFlags,
ownership::{TempRef, WeakRef},
statfs,
vfs::default_statfs,
SELINUX_MAGIC,
};
use std::{borrow::Cow, collections::BTreeMap, sync::Arc};
const SELINUX_PERMS: &[&str] = &["add", "find", "read", "set"];
struct SeLinuxFs;
impl FileSystemOps for SeLinuxFs {
fn statfs(&self, _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 {
fn new_fs(
current_task: &CurrentTask,
options: FileSystemOptions,
) -> Result<FileSystemHandle, Errno> {
let kernel = current_task.kernel();
let fs = FileSystem::new(kernel, CacheMode::Permanent, SeLinuxFs, options);
let mut dir = StaticDirectoryBuilder::new(&fs);
// There should always be a SecurityServer if SeLinuxFs is active.
let security_server = match kernel.security_server.as_ref() {
Some(security_server) => security_server,
None => {
return error!(EINVAL);
}
};
// Read-only files & directories, exposing SELinux internal state.
dir.entry(current_task, "checkreqprot", SeCheckReqProt::new_node(), mode!(IFREG, 0o644));
dir.entry(current_task, "class", SeLinuxClassDirectory::new(), mode!(IFDIR, 0o777));
dir.entry(
current_task,
"deny_unknown",
SeDenyUnknown::new_node(security_server.clone()),
mode!(IFREG, 0o444),
);
dir.entry(
current_task,
"reject_unknown",
SeRejectUnknown::new_node(security_server.clone()),
mode!(IFREG, 0o444),
);
dir.subdir(current_task, "initial_contexts", 0o555, |dir| {
for initial_sid in InitialSid::all_variants().into_iter() {
dir.entry(
current_task,
initial_sid.name(),
SeInitialContext::new_node(security_server.clone(), initial_sid),
mode!(IFREG, 0o444),
);
}
});
dir.entry(current_task, "mls", BytesFile::new_node(b"1".to_vec()), mode!(IFREG, 0o444));
dir.entry(
current_task,
"policy",
SePolicy::new_node(security_server.clone()),
mode!(IFREG, 0o600),
);
dir.entry(
current_task,
"policyvers",
BytesFile::new_node(format!("{}", SUPPORTED_POLICY_VERSION).as_bytes().to_vec()),
mode!(IFREG, 0o444),
);
dir.entry(
current_task,
"status",
// 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.
VmoFileNode::from_vmo(security_server.get_status_vmo()),
mode!(IFREG, 0o444),
);
// Write-only files used to configure and query SELinux.
dir.entry(current_task, "access", AccessFileNode::new(), mode!(IFREG, 0o666));
dir.entry(
current_task,
"context",
SeContext::new_node(security_server.clone()),
mode!(IFREG, 0o666),
);
dir.entry(current_task, "create", SeCreate::new_node(), mode!(IFREG, 0o666));
dir.entry(
current_task,
"load",
SeLoad::new_node(security_server.clone()),
mode!(IFREG, 0o600),
);
dir.entry(
current_task,
"commit_pending_bools",
SeLinuxCommitBooleans::new_node(security_server.clone()),
mode!(IFREG, 0o200),
);
// Read/write files allowing values to be queried or changed.
dir.entry(
current_task,
"booleans",
SeLinuxBooleansDirectory::new(security_server.clone()),
mode!(IFDIR, 0o555),
);
dir.entry(
current_task,
"enforce",
SeEnforce::new_node(security_server.clone()),
// TODO(b/297313229): Get mode from the container.
mode!(IFREG, 0o644),
);
// "/dev/null" equivalent used for file descriptors redirected by SELinux.
dir.entry_dev(current_task, "null", DeviceFileNode, mode!(IFCHR, 0o666), DeviceType::NULL);
dir.build_root();
Ok(fs)
}
}
struct SeLoad {
security_server: Arc<SecurityServer>,
}
impl SeLoad {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
BytesFile::new_node(Self { security_server })
}
}
impl BytesFileOps for SeLoad {
fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
track_stub!(TODO("https://fxbug.dev/322874969"), "ignoring selinux policy");
log_info!("Loading {} byte policy", data.len());
self.security_server.load_policy(data).map_err(|error| {
log_error!("Policy load error: {}", error);
errno!(EINVAL)
})
}
}
struct SePolicy {
security_server: Arc<SecurityServer>,
}
impl SePolicy {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
BytesFile::new_node(Self { security_server })
}
}
impl BytesFileOps for SePolicy {
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
Ok(self.security_server.get_binary_policy().into())
}
}
struct SeEnforce {
security_server: Arc<SecurityServer>,
}
impl SeEnforce {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
BytesFile::new_node(Self { security_server })
}
}
impl BytesFileOps for SeEnforce {
fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
let enforce = parse_unsigned_file::<u32>(&data)? != 0;
self.security_server.set_enforcing(enforce);
Ok(())
}
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
Ok(format!("{}", self.security_server.is_enforcing() as u32).as_bytes().to_vec().into())
}
}
struct SeDenyUnknown {
security_server: Arc<SecurityServer>,
}
impl SeDenyUnknown {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
BytesFile::new_node(Self { security_server })
}
}
impl BytesFileOps for SeDenyUnknown {
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
Ok(format!("{}", self.security_server.deny_unknown() as u32).as_bytes().to_vec().into())
}
}
struct SeRejectUnknown {
security_server: Arc<SecurityServer>,
}
impl SeRejectUnknown {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
BytesFile::new_node(Self { security_server })
}
}
impl BytesFileOps for SeRejectUnknown {
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
Ok(format!("{}", self.security_server.reject_unknown() as u32).as_bytes().to_vec().into())
}
}
struct SeCreate {
data: Mutex<Vec<u8>>,
}
impl SeCreate {
fn new_node() -> impl FsNodeOps {
BytesFile::new_node(Self { data: Mutex::default() })
}
}
impl BytesFileOps for SeCreate {
fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
*self.data.lock() = data;
Ok(())
}
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
Ok(self.data.lock().clone().into())
}
}
struct SeCheckReqProt;
impl SeCheckReqProt {
fn new_node() -> impl FsNodeOps {
BytesFile::new_node(Self {})
}
}
impl BytesFileOps for SeCheckReqProt {
fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
let _checkreqprot = parse_unsigned_file::<u32>(&data)? != 0;
track_stub!(TODO("https://fxbug.dev/322874766"), "selinux checkreqprot");
Ok(())
}
}
struct SeContext {
security_server: Arc<SecurityServer>,
}
impl SeContext {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
BytesFile::new_node(Self { security_server })
}
}
impl BytesFileOps for SeContext {
fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
// Validate that the `data` describe valid user, role, type, etc by attempting to create
// a SID from it.
let context = data.as_slice().trim_end_with(|c| c == '\0');
self.security_server.security_context_to_sid(context.into()).map_err(|_| errno!(EINVAL))?;
Ok(())
}
}
struct SeInitialContext {
security_server: Arc<SecurityServer>,
initial_sid: InitialSid,
}
impl SeInitialContext {
fn new_node(security_server: Arc<SecurityServer>, initial_sid: InitialSid) -> impl FsNodeOps {
BytesFile::new_node(Self { security_server, initial_sid })
}
}
impl BytesFileOps for SeInitialContext {
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
let sid = SecurityId::initial(self.initial_sid);
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())
}
}
}
struct AccessFile {
seqno: u64,
}
impl FileOps for AccessFile {
fileops_impl_nonseekable!();
fn read(
&self,
_locked: &mut Locked<'_, FileOpsCore>,
_file: &FileObject,
_current_task: &CurrentTask,
offset: usize,
data: &mut dyn OutputBuffer,
) -> Result<usize, Errno> {
debug_assert!(offset == 0);
// Format is allowed decided autitallow auditdeny seqno flags
// Everything but seqno must be in hexadecimal format and represents a bits field.
let content = format!("ffffffff ffffffff 0 ffffffff {} 0\n", self.seqno);
data.write(content.as_bytes())
}
fn write(
&self,
_locked: &mut Locked<'_, WriteOps>,
_file: &FileObject,
_current_task: &CurrentTask,
offset: usize,
data: &mut dyn InputBuffer,
) -> Result<usize, Errno> {
debug_assert!(offset == 0);
Ok(data.drain())
}
}
struct DeviceFileNode;
impl FsNodeOps for DeviceFileNode {
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> {
unreachable!("Special nodes cannot be opened.");
}
}
struct AccessFileNode {
seqno: Mutex<u64>,
}
impl AccessFileNode {
fn new() -> Self {
Self { seqno: Mutex::new(0) }
}
}
impl FsNodeOps for AccessFileNode {
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> {
let seqno = {
let mut writer = self.seqno.lock();
*writer += 1;
*writer
};
Ok(Box::new(AccessFile { seqno }))
}
}
struct SeLinuxBooleansDirectory {
security_server: Arc<SecurityServer>,
}
impl SeLinuxBooleansDirectory {
fn new(security_server: Arc<SecurityServer>) -> Arc<Self> {
Arc::new(Self { security_server })
}
}
impl FsNodeOps for Arc<SeLinuxBooleansDirectory> {
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,
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) {
Ok(node.fs().create_node(
current_task,
SeLinuxBoolean::new_node(self.security_server.clone(), utf8_name),
FsNodeInfo::new_factory(mode!(IFREG, 0o644), current_task.as_fscred()),
))
} else {
error!(ENOENT)
}
}
}
impl FileOps for SeLinuxBooleansDirectory {
fileops_impl_directory!();
fn seek(
&self,
_file: &FileObject,
_current_task: &CurrentTask,
current_offset: off_t,
target: SeekTarget,
) -> Result<off_t, Errno> {
unbounded_seek(current_offset, target)
}
fn readdir(
&self,
file: &FileObject,
_current_task: &CurrentTask,
sink: &mut dyn DirentSink,
) -> Result<(), Errno> {
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.next_node_id(),
/* next offset = */ sink.offset() + 1,
DirectoryEntryType::REG,
FsString::from(name.as_bytes()).as_ref(),
)?;
}
Ok(())
}
}
struct SeLinuxBoolean {
security_server: Arc<SecurityServer>,
name: String,
}
impl SeLinuxBoolean {
fn new_node(security_server: Arc<SecurityServer>, name: String) -> impl FsNodeOps {
BytesFile::new_node(SeLinuxBoolean { security_server, name })
}
}
impl BytesFileOps for SeLinuxBoolean {
fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
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> {
// 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).as_bytes().to_vec().into())
}
}
struct SeLinuxCommitBooleans {
security_server: Arc<SecurityServer>,
}
impl SeLinuxCommitBooleans {
fn new_node(security_server: Arc<SecurityServer>) -> impl FsNodeOps {
BytesFile::new_node(SeLinuxCommitBooleans { security_server })
}
}
impl BytesFileOps for SeLinuxCommitBooleans {
fn write(&self, _current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
// "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 SeLinuxClassDirectory {
entries: Mutex<BTreeMap<FsString, FsNodeHandle>>,
}
impl SeLinuxClassDirectory {
fn new() -> Arc<Self> {
Arc::new(Self { entries: Mutex::new(BTreeMap::new()) })
}
}
impl FsNodeOps for Arc<SeLinuxClassDirectory> {
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(VecDirectory::new_file(
self.entries
.lock()
.iter()
.map(|(name, node)| VecDirectoryEntry {
entry_type: DirectoryEntryType::DIR,
name: name.clone(),
inode: Some(node.node_id),
})
.collect(),
))
}
fn lookup(
&self,
node: &FsNode,
current_task: &CurrentTask,
name: &FsStr,
) -> Result<FsNodeHandle, Errno> {
let mut entries = self.entries.lock();
let next_index = entries.len() + 1;
Ok(entries
.entry(name.to_owned())
.or_insert_with(|| {
let index = format!("{next_index}\n").into_bytes();
let fs = node.fs();
let mut dir = StaticDirectoryBuilder::new(&fs);
dir.entry(current_task, "index", BytesFile::new_node(index), mode!(IFREG, 0o444));
dir.subdir(current_task, "perms", 0o555, |perms| {
for (i, perm) in SELINUX_PERMS.iter().enumerate() {
let node = BytesFile::new_node(format!("{}\n", i + 1).into_bytes());
perms.entry(current_task, perm, node, mode!(IFREG, 0o444));
}
});
dir.set_mode(mode!(IFDIR, 0o555));
dir.build(current_task)
})
.clone())
}
}
pub fn selinux_proc_attrs(
current_task: &CurrentTask,
task: &TempRef<'_, Task>,
dir: &mut StaticDirectoryBuilder<'_>,
) {
use SeProcAttrNodeType::*;
dir.entry(current_task, "current", Current.new_node(task), mode!(IFREG, 0o666));
dir.entry(current_task, "exec", Exec.new_node(task), mode!(IFREG, 0o666));
dir.entry(current_task, "fscreate", FsCreate.new_node(task), mode!(IFREG, 0o666));
dir.entry(current_task, "keycreate", KeyCreate.new_node(task), mode!(IFREG, 0o666));
dir.entry(current_task, "prev", Previous.new_node(task), mode!(IFREG, 0o666));
dir.entry(current_task, "sockcreate", SockCreate.new_node(task), mode!(IFREG, 0o666));
}
#[derive(Debug)]
enum SeProcAttrNodeType {
Current,
Exec,
FsCreate,
KeyCreate,
Previous,
SockCreate,
}
struct SeProcAttrNode {
attr: SeProcAttrNodeType,
task: WeakRef<Task>,
}
impl SeProcAttrNodeType {
fn new_node(self, task: &TempRef<'_, Task>) -> impl FsNodeOps {
BytesFile::new_node(SeProcAttrNode { attr: self, task: WeakRef::from(task) })
}
}
impl BytesFileOps for SeProcAttrNode {
fn write(&self, current_task: &CurrentTask, data: Vec<u8>) -> Result<(), Errno> {
let task = Task::from_weak(&self.task)?;
// If SELinux is disabled then no writes are accepted.
let security_server = task.kernel().security_server.as_ref().ok_or(errno!(EINVAL))?;
// If the current task is not the target then writes are not allowed.
if current_task.temp_task() != task {
return error!(EPERM);
}
// Attempt to convert the Security Context string to a SID.
// Writes that consist of a single NUL or a newline clear the SID.
let context = data.as_slice().trim_end_with(|c| c == '\0');
let sid = match context {
b"\x0a" => None,
slice => {
Some(security_server.security_context_to_sid(slice).map_err(|_| errno!(EINVAL))?)
}
};
// SELinux is enabled, so the task must have `selinux_state`. Lock it for writing
// and update it.
let mut tg = task.thread_group.write();
use SeProcAttrNodeType::*;
match self.attr {
Current => tg.selinux_state.current_sid = sid.ok_or(errno!(EINVAL))?,
Exec => tg.selinux_state.exec_sid = sid,
FsCreate => tg.selinux_state.fscreate_sid = sid,
KeyCreate => tg.selinux_state.keycreate_sid = sid,
Previous => {
return error!(EINVAL);
}
SockCreate => tg.selinux_state.sockcreate_sid = sid,
};
Ok(())
}
fn read(&self, _current_task: &CurrentTask) -> Result<Cow<'_, [u8]>, Errno> {
use SeProcAttrNodeType::*;
let task = Task::from_weak(&self.task)?;
// If SELinux is disabled then all reads are rejected, except for "current".
match &task.kernel().security_server {
Some(security_server) => {
// Read the specified SELinux attribute's SID.
let sid = {
let tg = task.thread_group.read();
match self.attr {
Current => Some(tg.selinux_state.current_sid),
Exec => tg.selinux_state.exec_sid,
FsCreate => tg.selinux_state.fscreate_sid,
KeyCreate => tg.selinux_state.keycreate_sid,
Previous => Some(tg.selinux_state.previous_sid),
SockCreate => tg.selinux_state.sockcreate_sid,
}
};
// Convert it to a Security Context string.
let security_context =
sid.and_then(|sid| security_server.sid_to_security_context(sid));
Ok(security_context.unwrap_or_else(Vec::new).into())
}
None => match self.attr {
Current => Ok(b"unconfined".to_vec().into()),
_ => error!(EINVAL),
},
}
}
}
/// # Panics
///
/// Will panic if the supplied `kern` is not configured with SELinux enabled.
pub fn selinux_fs(current_task: &CurrentTask, options: FileSystemOptions) -> &FileSystemHandle {
current_task.kernel().selinux_fs.get_or_init(|| {
SeLinuxFs::new_fs(current_task, options).expect("failed to construct selinuxfs")
})
}