use crate::{ error::{KernelError, Result}, proc::{ caps::{Capabilities, CapabilitiesFlags}, ids::{Gid, Uid}, }, }; use super::{FileType, InodeId}; use bitflags::bitflags; use core::time::Duration; bitflags::bitflags! { #[derive(Debug, Clone, Copy)] pub struct AccessMode: i32 { /// Execution is permitted const X_OK = 1; /// Writing is permitted const W_OK = 2; /// Reading is permitted const R_OK = 4; } } bitflags! { #[derive(Clone, Copy, Debug)] pub struct FilePermissions: u16 { // Owner permissions const S_IRUSR = 0o400; // Read permission, owner const S_IWUSR = 0o200; // Write permission, owner const S_IXUSR = 0o100; // Execute/search permission, owner // Group permissions const S_IRGRP = 0o040; // Read permission, group const S_IWGRP = 0o020; // Write permission, group const S_IXGRP = 0o010; // Execute/search permission, group // Others permissions const S_IROTH = 0o004; // Read permission, others const S_IWOTH = 0o002; // Write permission, others const S_IXOTH = 0o001; // Execute/search permission, others // Optional: sticky/setuid/setgid bits const S_ISUID = 0o4000; // Set-user-ID on execution const S_ISGID = 0o2000; // Set-group-ID on execution const S_ISVTX = 0o1000; // Sticky bit } } /// Represents file metadata, similar to `stat`. #[derive(Debug, Clone)] pub struct FileAttr { pub id: InodeId, pub size: u64, pub block_size: u32, pub blocks: u64, pub atime: Duration, // Access time (e.g., seconds since epoch) pub btime: Duration, // Creation time pub mtime: Duration, // Modification time pub ctime: Duration, // Change time pub file_type: FileType, pub mode: FilePermissions, pub nlinks: u32, pub uid: Uid, pub gid: Gid, } impl Default for FileAttr { fn default() -> Self { Self { id: InodeId::dummy(), size: 0, block_size: 0, blocks: 0, atime: Duration::new(0, 0), btime: Duration::new(0, 0), mtime: Duration::new(0, 0), ctime: Duration::new(0, 0), file_type: FileType::File, mode: FilePermissions::empty(), nlinks: 1, uid: Uid::new_root(), gid: Gid::new_root_group(), } } } impl FileAttr { /// Checks if a given set of credentials has the requested access permissions for this file. /// /// # Arguments /// * `uid` - The user-ID that will be checked against this file's uid field. /// * `gid` - The group-ID that will be checked against this file's uid field. /// * `caps` - The capabilities of the user. /// * `requested_mode` - A bitmask of `AccessMode` flags (`R_OK`, `W_OK`, `X_OK`) to check. pub fn check_access( &self, uid: Uid, gid: Gid, caps: Capabilities, requested_mode: AccessMode, ) -> Result<()> { // For filesystem related tasks, the CAP_DAC_OVERRIDE bypasses all permission checks. if caps.is_capable(CapabilitiesFlags::CAP_DAC_OVERRIDE) { return Ok(()); } // root (UID 0) bypasses most permission checks. For execute, at // least one execute bit must be set. if uid.is_root() { if requested_mode.contains(AccessMode::X_OK) { // Root still needs at least one execute bit to be set for X_OK if self.mode.intersects( FilePermissions::S_IXUSR | FilePermissions::S_IXGRP | FilePermissions::S_IXOTH, ) { return Ok(()); } } else { return Ok(()); } } // Determine which set of permission bits to use (owner, group, or other) let perms_to_check = if self.uid == uid { // User is the owner self.mode } else if self.gid == gid { // User is in the file's group. Shift group bits to align with owner bits for easier checking. FilePermissions::from_bits_truncate(self.mode.bits() << 3) } else { // Others. Shift other bits to align with owner bits. FilePermissions::from_bits_truncate(self.mode.bits() << 6) }; if requested_mode.contains(AccessMode::R_OK) && !perms_to_check.contains(FilePermissions::S_IRUSR) && !caps.is_capable(CapabilitiesFlags::CAP_DAC_READ_SEARCH) { return Err(KernelError::NotPermitted); } if requested_mode.contains(AccessMode::W_OK) && !perms_to_check.contains(FilePermissions::S_IWUSR) { return Err(KernelError::NotPermitted); } if requested_mode.contains(AccessMode::X_OK) && !perms_to_check.contains(FilePermissions::S_IXUSR) && (self.file_type != FileType::Directory // CAP_DAC_READ_SEARCH allows directory search as well || !caps.is_capable(CapabilitiesFlags::CAP_DAC_READ_SEARCH)) { return Err(KernelError::NotPermitted); } Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::error::KernelError; const ROOT_UID: Uid = Uid::new(0); const ROOT_GID: Gid = Gid::new(0); const OWNER_UID: Uid = Uid::new(1000); const OWNER_GID: Gid = Gid::new(1000); const GROUP_MEMBER_UID: Uid = Uid::new(1001); const FILE_GROUP_GID: Gid = Gid::new(2000); const OTHER_UID: Uid = Uid::new(1002); const OTHER_GID: Gid = Gid::new(3000); fn setup_file(mode: FilePermissions) -> FileAttr { FileAttr { uid: OWNER_UID, gid: FILE_GROUP_GID, mode, ..Default::default() } } #[test] fn root_can_read_without_perms() { let file = setup_file(FilePermissions::empty()); assert!( file.check_access( ROOT_UID, ROOT_GID, Capabilities::new_empty(), AccessMode::R_OK ) .is_ok() ); } #[test] fn root_can_write_without_perms() { let file = setup_file(FilePermissions::empty()); assert!( file.check_access( ROOT_UID, ROOT_GID, Capabilities::new_empty(), AccessMode::W_OK ) .is_ok() ); } #[test] fn root_cannot_execute_if_no_exec_bits_are_set() { let file = setup_file(FilePermissions::S_IRUSR | FilePermissions::S_IWUSR); let result = file.check_access( ROOT_UID, ROOT_GID, Capabilities::new_empty(), AccessMode::X_OK, ); assert!(matches!(result, Err(KernelError::NotPermitted))); } #[test] fn root_can_execute_if_owner_exec_bit_is_set() { let file = setup_file(FilePermissions::S_IXUSR); assert!( file.check_access( ROOT_UID, ROOT_GID, Capabilities::new_empty(), AccessMode::X_OK ) .is_ok() ); } #[test] fn root_can_execute_if_group_exec_bit_is_set() { let file = setup_file(FilePermissions::S_IXGRP); assert!( file.check_access( ROOT_UID, ROOT_GID, Capabilities::new_empty(), AccessMode::X_OK ) .is_ok() ); } #[test] fn root_can_execute_if_other_exec_bit_is_set() { let file = setup_file(FilePermissions::S_IXOTH); assert!( file.check_access( ROOT_UID, ROOT_GID, Capabilities::new_empty(), AccessMode::X_OK ) .is_ok() ); } #[test] fn owner_can_read_when_permitted() { let file = setup_file(FilePermissions::S_IRUSR); assert!( file.check_access( OWNER_UID, OWNER_GID, Capabilities::new_empty(), AccessMode::R_OK ) .is_ok() ); } #[test] fn owner_cannot_read_when_denied() { let file = setup_file(FilePermissions::S_IWUSR | FilePermissions::S_IXUSR); let result = file.check_access( OWNER_UID, OWNER_GID, Capabilities::new_empty(), AccessMode::R_OK, ); assert!(matches!(result, Err(KernelError::NotPermitted))); } #[test] fn owner_can_write_when_permitted() { let file = setup_file(FilePermissions::S_IWUSR); assert!( file.check_access( OWNER_UID, OWNER_GID, Capabilities::new_empty(), AccessMode::W_OK ) .is_ok() ); } #[test] fn owner_cannot_write_when_denied() { let file = setup_file(FilePermissions::S_IRUSR); let result = file.check_access( OWNER_UID, OWNER_GID, Capabilities::new_empty(), AccessMode::W_OK, ); assert!(matches!(result, Err(KernelError::NotPermitted))); } #[test] fn owner_can_read_write_execute_when_permitted() { let file = setup_file( FilePermissions::S_IRUSR | FilePermissions::S_IWUSR | FilePermissions::S_IXUSR, ); let mode = AccessMode::R_OK | AccessMode::W_OK | AccessMode::X_OK; assert!( file.check_access(OWNER_UID, OWNER_GID, Capabilities::new_empty(), mode) .is_ok() ); } #[test] fn owner_access_denied_if_one_of_many_perms_is_missing() { let file = setup_file(FilePermissions::S_IRUSR | FilePermissions::S_IXUSR); let mode = AccessMode::R_OK | AccessMode::W_OK | AccessMode::X_OK; // Requesting Write is denied let result = file.check_access(OWNER_UID, OWNER_GID, Capabilities::new_empty(), mode); assert!(matches!(result, Err(KernelError::NotPermitted))); } #[test] fn group_member_can_read_when_permitted() { let file = setup_file(FilePermissions::S_IRGRP); assert!( file.check_access( GROUP_MEMBER_UID, FILE_GROUP_GID, Capabilities::new_empty(), AccessMode::R_OK ) .is_ok() ); } #[test] fn group_member_cannot_write_when_owner_can() { let file = setup_file(FilePermissions::S_IWUSR | FilePermissions::S_IRGRP); let result = file.check_access( GROUP_MEMBER_UID, FILE_GROUP_GID, Capabilities::new_empty(), AccessMode::W_OK, ); assert!(matches!(result, Err(KernelError::NotPermitted))); } #[test] fn group_member_cannot_read_when_denied() { let file = setup_file(FilePermissions::S_IWGRP); let result = file.check_access( GROUP_MEMBER_UID, FILE_GROUP_GID, Capabilities::new_empty(), AccessMode::R_OK, ); assert!(matches!(result, Err(KernelError::NotPermitted))); } #[test] fn other_can_execute_when_permitted() { let file = setup_file(FilePermissions::S_IXOTH); assert!( file.check_access( OTHER_UID, OTHER_GID, Capabilities::new_empty(), AccessMode::X_OK ) .is_ok() ); } #[test] fn other_cannot_read_when_only_owner_and_group_can() { let file = setup_file(FilePermissions::S_IRUSR | FilePermissions::S_IRGRP); let result = file.check_access( OTHER_UID, OTHER_GID, Capabilities::new_empty(), AccessMode::R_OK, ); assert!(matches!(result, Err(KernelError::NotPermitted))); } #[test] fn other_cannot_write_when_denied() { let file = setup_file(FilePermissions::S_IROTH); let result = file.check_access( OTHER_UID, OTHER_GID, Capabilities::new_empty(), AccessMode::W_OK, ); assert!(matches!(result, Err(KernelError::NotPermitted))); } #[test] fn no_requested_mode_is_always_ok() { // Checking for nothing should always succeed if the file exists. let file = setup_file(FilePermissions::empty()); assert!( file.check_access( OTHER_UID, OTHER_GID, Capabilities::new_empty(), AccessMode::empty() ) .is_ok() ); } #[test] fn user_in_different_group_is_treated_as_other() { let file = setup_file(FilePermissions::S_IROTH); // Only other can read // This user is not the owner and not in the file's group. assert!( file.check_access( GROUP_MEMBER_UID, OTHER_GID, Capabilities::new_empty(), AccessMode::R_OK ) .is_ok() ); } #[test] fn cap_dac_override_can_read_write_exec_without_perms() { let file = setup_file(FilePermissions::empty()); let mode = AccessMode::R_OK | AccessMode::W_OK | AccessMode::X_OK; assert!( file.check_access( ROOT_UID, ROOT_GID, Capabilities::new_cap(CapabilitiesFlags::CAP_DAC_OVERRIDE), mode, ) .is_ok() ); } #[test] fn cap_dac_read_search_can_read_without_perms() { let file = setup_file(FilePermissions::empty()); assert!( file.check_access( OTHER_UID, OTHER_GID, Capabilities::new_cap(CapabilitiesFlags::CAP_DAC_READ_SEARCH), AccessMode::R_OK, ) .is_ok() ); } #[test] fn cap_dac_read_search_cannot_write_without_perms() { let file = setup_file(FilePermissions::empty()); let result = file.check_access( OTHER_UID, OTHER_GID, Capabilities::new_cap(CapabilitiesFlags::CAP_DAC_READ_SEARCH), AccessMode::W_OK, ); assert!(matches!(result, Err(KernelError::NotPermitted))); } #[test] fn cap_dac_read_search_cannot_exec_without_perms() { let file = setup_file(FilePermissions::empty()); let result = file.check_access( OTHER_UID, OTHER_GID, Capabilities::new_cap(CapabilitiesFlags::CAP_DAC_READ_SEARCH), AccessMode::X_OK, ); assert!(matches!(result, Err(KernelError::NotPermitted))); } }