blob: 517b66a3d81c8ea5d0b2b1b3861412f0c1c7cd53 [file]
// Copyright 2026 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 starnix_core::security::{self, Auditable, PermissionFlags};
use starnix_core::task::CurrentTask;
use starnix_core::vfs::pseudo::simple_directory::{SimpleDirectory, SimpleDirectoryMutator};
use starnix_core::vfs::{
AppendLockWriteGuard, CheckAccessReason, FileOps, FileSystemHandle, FsNode, FsNodeHandle,
FsNodeInfo, FsNodeOps, fs_node_impl_not_dir,
};
use starnix_sync::DynamicLockDepRwLock;
use starnix_uapi::auth::Capabilities;
use starnix_uapi::errors::Errno;
use starnix_uapi::file_mode::{Access, FileMode};
use starnix_uapi::open_flags::OpenFlags;
use starnix_uapi::{error, gid_t, uid_t};
use std::sync::Arc;
/// Wrapper around leaf [`FsNodeOps`] nodes under `/proc/sys` that enforces sysctl access-control
/// semantics.
///
/// Specifically:
/// * `CAP_DAC_OVERRIDE` does not grant write access to `/proc/sys` files.
/// * Non-owner processes holding `ADMIN_CAP` (`CAP_NET_ADMIN` under `/proc/sys/net`,
/// `CAP_SYS_ADMIN` elsewhere under `/proc/sys`) are granted the file owner's access permissions.
/// * Read-only files (`0444`) reject writes even from root (`uid == 0`) or processes with
/// administrative capabilities.
struct SysctlFsNodeOps<T: FsNodeOps, const ADMIN_CAP: u32> {
inner: T,
}
impl<T: FsNodeOps, const ADMIN_CAP: u32> SysctlFsNodeOps<T, ADMIN_CAP> {
fn new(inner: T) -> Self {
Self { inner }
}
}
impl<T: FsNodeOps, const ADMIN_CAP: u32> FsNodeOps for SysctlFsNodeOps<T, ADMIN_CAP> {
fs_node_impl_not_dir!();
fn create_file_ops(
&self,
node: &FsNode,
current_task: &CurrentTask,
flags: OpenFlags,
) -> Result<Box<dyn FileOps>, Errno> {
self.inner.create_file_ops(node, current_task, flags)
}
fn truncate(
&self,
guard: &AppendLockWriteGuard<'_>,
node: &FsNode,
current_task: &CurrentTask,
length: u64,
) -> Result<(), Errno> {
self.inner.truncate(guard, node, current_task, length)
}
fn check_access(
&self,
node: &FsNode,
current_task: &CurrentTask,
permission_flags: PermissionFlags,
info: &DynamicLockDepRwLock<FsNodeInfo>,
_reason: CheckAccessReason,
audit_context: Auditable<'_>,
) -> Result<(), Errno> {
let (node_uid, node_gid, node_mode) = {
let info = info.read();
(info.uid, info.gid, info.mode)
};
check_sysctl_access(
current_task,
permission_flags.as_access(),
node_uid,
node_gid,
node_mode,
Capabilities::from_bits_truncate(1 << ADMIN_CAP),
)?;
security::fs_node_permission(current_task, node, permission_flags, audit_context)
}
}
fn check_sysctl_access(
current_task: &CurrentTask,
requested: Access,
node_uid: uid_t,
node_gid: gid_t,
node_mode: FileMode,
admin_capability: Capabilities,
) -> Result<(), Errno> {
let (fsuid, is_in_group) = {
let current_creds = current_task.current_creds();
(current_creds.fsuid, current_creds.is_in_group(node_gid))
};
let granted = if fsuid == node_uid {
node_mode.user_access()
} else if is_in_group {
node_mode.group_access()
} else {
node_mode.other_access()
};
let mut not_granted = requested;
not_granted.remove(granted);
if not_granted.is_empty() {
return Ok(());
}
// Non-owner processes holding the directory's administrative capability (`CAP_NET_ADMIN` under
// `/proc/sys/net` or `CAP_SYS_ADMIN` elsewhere under `/proc/sys`) are granted the owner's
// access permissions (such as write access on 0644 files), whereas `CAP_DAC_OVERRIDE` is
// ignored.
if security::check_task_capable(current_task, admin_capability).is_ok() {
not_granted.remove(node_mode.user_access());
}
if not_granted.is_empty() { Ok(()) } else { error!(EACCES) }
}
/// Directory wrapper around [`SimpleDirectory`] that enforces `/proc/sys` access controls on leaf
/// entries added via [`SysctlDirectory::edit`] or [`SysctlDirectoryMutator`].
pub(crate) struct SysctlDirectory<const ADMIN_CAP: u32> {
directory: Arc<SimpleDirectory>,
}
impl<const ADMIN_CAP: u32> SysctlDirectory<ADMIN_CAP> {
pub fn new() -> Self {
Self { directory: SimpleDirectory::new() }
}
pub fn edit(
&self,
fs: &FileSystemHandle,
callback: impl FnOnce(&SysctlDirectoryMutator<'_, ADMIN_CAP>),
) {
let mutator = SysctlDirectoryMutator::new(fs.clone(), self);
callback(&mutator);
}
pub fn into_node(self, fs: &FileSystemHandle, mode: u32) -> FsNodeHandle {
self.directory.into_node(fs, mode)
}
}
/// Builder wrapper around [`SimpleDirectoryMutator`] that automatically wraps leaf files in
/// [`SysctlFsNodeOps`] using the parent [`SysctlDirectory`]'s administrative capability.
pub(crate) struct SysctlDirectoryMutator<'a, const ADMIN_CAP: u32> {
fs: FileSystemHandle,
directory: &'a SysctlDirectory<ADMIN_CAP>,
mutator: SimpleDirectoryMutator,
}
impl<'a, const ADMIN_CAP: u32> SysctlDirectoryMutator<'a, ADMIN_CAP> {
pub fn new(fs: FileSystemHandle, directory: &'a SysctlDirectory<ADMIN_CAP>) -> Self {
let mutator = SimpleDirectoryMutator::new(fs.clone(), directory.directory.clone());
Self { fs, directory, mutator }
}
pub fn entry(&self, name: &str, ops: impl FsNodeOps, mode: FileMode) {
if mode.is_dir() {
self.mutator.entry(name, ops, mode);
} else {
self.mutator.entry(name, SysctlFsNodeOps::<_, ADMIN_CAP>::new(ops), mode);
}
}
pub fn subdir(
&self,
name: &str,
mode: u32,
build_subdir: impl FnOnce(&SysctlDirectoryMutator<'_, ADMIN_CAP>),
) {
self.subdir_with_capability::<ADMIN_CAP>(name, mode, build_subdir);
}
pub fn subdir_with_capability<const SUBDIR_CAP: u32>(
&self,
name: &str,
mode: u32,
build_subdir: impl FnOnce(&SysctlDirectoryMutator<'_, SUBDIR_CAP>),
) {
let sub_dir = SysctlDirectory::<SUBDIR_CAP> {
directory: self.directory.directory.subdir(&self.fs, name.into(), mode),
};
let sub_mutator = SysctlDirectoryMutator::new(self.fs.clone(), &sub_dir);
build_subdir(&sub_mutator);
}
}