mirror of
https://github.com/hexagonal-sun/moss-kernel.git
synced 2026-05-05 06:25:49 -04:00
494 lines
15 KiB
Rust
494 lines
15 KiB
Rust
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)));
|
|
}
|
|
}
|