| // 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); |
| } |
| } |