diff --git a/experiment/selinux/src/error.rs b/experiment/selinux/src/error.rs new file mode 100644 index 000000000..4596d0ded --- /dev/null +++ b/experiment/selinux/src/error.rs @@ -0,0 +1,55 @@ +use std::path::PathBuf; + +#[derive(Debug, thiserror::Error)] +pub enum SELinuxError { + #[error("Failed to set file label for SELinux: {0}")] + SetFileLabel(String), + #[error("Failed to lset file label for SELinux: {0}")] + LSetFileLabel(String), + #[error("Failed to get file label for SELinux: {0}")] + FileLabel(String), + #[error("Failed to get lfile label for SELinux: {0}")] + LFileLabel(String), + #[error("Failed to call is_proc_handle for SELinux: {0}")] + IsProcHandle(String), + #[error("Failed to call read_con_fd for SELinux: {0}")] + ReadConFd(String), + #[error("Failed to call read_con for SELinux: {0}")] + ReadCon(String), + #[error("Failed to call write_con for SELinux: {0}")] + WriteCon(String), + #[error("Failed to find the index for a given class: {0}")] + ClassIndex(String), + #[error("Failed to call peer_label for SELinux: {0}")] + PeerLabel(String), + #[error("Failed to call open_context_file for SELinux: {0}")] + OpenContextFile(String), + #[error("Failed to set enforce mode of SELinux: {0}")] + SetEnforceMode(String), + #[error("Failed to read config file of SELinux: {0}")] + GetConfigKey(String), + #[error("Invalid format for SELinux label: {0}")] + InvalidSELinuxLabel(String), + #[error("Failed to load SELinux labels: {0}")] + LoadLabels(String), + #[error("Failed to load SELinux config: {0}")] + LoadConfig(String), + #[error("SELinux setting error: {0}")] + SELinuxSettingError(#[from] SELinuxSettingError), +} + +#[derive(Debug, thiserror::Error)] +pub enum SELinuxSettingError { + #[error("SELinux is not installed")] + NotInstalled, + #[error("Enforce file in SELinux not found: {0}")] + EnforceFileNotFound(PathBuf), + #[error("Invalid SELinux mode: {0}")] + InvalidMode(String), + #[error("Failed to set enforce mode of SELinux: {0}")] + SetEnforceMode(String), + #[error("Failed to load SELinux config: {0}")] + LoadConfig(String), + #[error("Failed to read config file of SELinux: {0}")] + GetConfigKey(String), +} diff --git a/experiment/selinux/src/selinux_label.rs b/experiment/selinux/src/label.rs similarity index 85% rename from experiment/selinux/src/selinux_label.rs rename to experiment/selinux/src/label.rs index 04180fec6..3be527436 100644 --- a/experiment/selinux/src/selinux_label.rs +++ b/experiment/selinux/src/label.rs @@ -1,12 +1,10 @@ -use crate::selinux::*; use crate::tools::PathXattr; use crate::tools::*; +use crate::{selinux::*, SELinuxError}; use nix::sys::socket::getsockopt; use std::convert::TryFrom; -use std::io::{BufRead, BufReader}; use std::os::fd::AsFd; use std::path::Path; -use std::sync::atomic::Ordering; const XATTR_NAME_SELINUX: &str = "security.selinux"; const KEY_LABEL_PATH: &str = "/proc/self/attr/keycreate"; @@ -60,7 +58,7 @@ impl TryFrom for SELinuxLabel { } // This impl is for methods related to labels in SELinux struct. -impl SELinux { +impl<'a> SELinux<'a> { // set_file_label sets the SELinux label for this path, following symlinks, or returns an error. pub fn set_file_label + PathXattr>( fpath: P, @@ -235,7 +233,7 @@ impl SELinux { // kvm_container_labels returns the default processLabel and mountLabel to be used // for kvm containers by the calling process. - pub fn kvm_container_labels(&mut self) -> (Option, Option) { + pub fn kvm_container_labels(&'a mut self) -> (Option<&SELinuxLabel>, Option<&SELinuxLabel>) { let process_label = Self::label(self, "kvm_process").or_else(|| Self::label(self, "process")); (process_label, Self::label(self, "file")) @@ -244,7 +242,7 @@ impl SELinux { // init_container_labels returns the default processLabel and file labels to be // used for containers running an init system like systemd by the calling process. - pub fn init_container_labels(&mut self) -> (Option, Option) { + pub fn init_container_labels(&'a mut self) -> (Option<&SELinuxLabel>, Option<&SELinuxLabel>) { let process_label = Self::label(self, "init_process").or_else(|| Self::label(self, "process")); (process_label, Self::label(self, "file")) @@ -253,61 +251,28 @@ impl SELinux { // container_labels returns an allocated processLabel and fileLabel to be used for // container labeling by the calling process. - pub fn container_labels(&mut self) -> (Option, Option) { - if !Self::get_enabled(self) { - return (None, None); - } - let process_label = Self::label(self, "process"); - let file_label = Self::label(self, "file"); + pub fn container_labels(&'a mut self) -> (Option<&SELinuxLabel>, Option<&SELinuxLabel>) { + // let process_label = Self::label(self, "process"); + // let file_label = Self::label(self, "file"); + let process_label = self.labels.get("process"); + let file_label = self.labels.get("file"); if process_label.is_none() || file_label.is_none() { return (process_label, file_label); } - let mut read_only_file_label = Self::label(self, "ro_file"); - if read_only_file_label.is_none() { - read_only_file_label = file_label.clone(); - } - self.read_only_file_label = read_only_file_label; + self.read_only_file_label = match self.labels.get("ro_file") { + None => file_label, + Some(ro_file_label) => Some(ro_file_label), + }; (process_label, file_label) // TODO: use addMcs } // This function returns the value of given key on selinux context - fn label(&mut self, key: &str) -> Option { - if !self.load_labels_init_done.load(Ordering::SeqCst) { - Self::load_labels(self); - self.load_labels_init_done.store(true, Ordering::SeqCst); - } - self.labels.get(key).cloned() - } - - // This function loads context file and reads labels and stores it. - fn load_labels(&mut self) { - // The context file should have pairs of key and value like below. - // ---------- - // process = "system_u:system_r:container_t:s0" - // file = "system_u:object_r:container_file_t:s0" - // ---------- - if let Ok(file) = Self::open_context_file(self) { - let reader = BufReader::new(file); - for line in reader.lines().map_while(Result::ok) { - let line = line.trim(); - if line.is_empty() || line.starts_with(';') || line.starts_with('#') { - continue; - } - let fields: Vec<&str> = line.splitn(2, '=').collect(); - if fields.len() != 2 { - continue; - } - let key = fields[0].trim().to_string(); - let value = fields[1].trim_matches('"').trim().to_string(); - if let Ok(value_label) = SELinuxLabel::try_from(value) { - self.labels.insert(key, value_label); - } - } - } + fn label(&'a self, key: &str) -> Option<&'a SELinuxLabel> { + self.labels.get(key) } // format_mount_label returns a string to be used by the mount command. diff --git a/experiment/selinux/src/lib.rs b/experiment/selinux/src/lib.rs index 9a9bfd990..e063faa5a 100644 --- a/experiment/selinux/src/lib.rs +++ b/experiment/selinux/src/lib.rs @@ -1,5 +1,10 @@ +pub mod error; +pub mod label; +pub mod mode; pub mod selinux; -pub mod selinux_label; +pub mod setting; pub mod tools; +pub use error::*; +pub use mode::SELinuxMode; pub use selinux::SELinux; diff --git a/experiment/selinux/src/main.rs b/experiment/selinux/src/main.rs index 9a437a3b2..25889394e 100644 --- a/experiment/selinux/src/main.rs +++ b/experiment/selinux/src/main.rs @@ -1,31 +1,24 @@ -use selinux::selinux::*; -use selinux::selinux_label::*; use std::fs::File; use std::path::Path; +use selinux::{label::*, selinux::*, setting::SELinuxSetting, SELinuxError, SELinuxMode}; + fn main() -> Result<(), SELinuxError> { - let mut selinux_instance: SELinux = SELinux::new(); + let setting = SELinuxSetting::try_default()?; + println!("current enforce mode is: {}", setting.enforce_mode()?); - if selinux_instance.get_enabled() { - println!("selinux is enabled"); + let selinux: SELinux = SELinux::try_default(&setting)?; + if selinux.get_enabled() { + println!("SELinux is enabled"); } else { - println!("selinux is not enabled"); - - match selinux_instance.set_enforce_mode(SELinuxMode::PERMISSIVE) { + println!("SELinux is not enabled"); + match setting.set_enforce_mode(SELinuxMode::PERMISSIVE) { Ok(_) => println!("set selinux mode as permissive"), - Err(e) => println!("{}", e), - } + Err(e) => return Err(SELinuxError::from(e)), + }; } - println!( - "default enforce mode is: {}", - selinux_instance.default_enforce_mode() - ); - println!( - "current enforce mode is: {}", - selinux_instance.enforce_mode() - ); - match selinux_instance.current_label() { + match selinux.current_label() { Ok(l) => println!("SELinux label of current process is: {}", l), Err(e) => println!("{}", e), } diff --git a/experiment/selinux/src/mode.rs b/experiment/selinux/src/mode.rs new file mode 100644 index 000000000..bf2765d58 --- /dev/null +++ b/experiment/selinux/src/mode.rs @@ -0,0 +1,53 @@ +use std::fmt; + +#[derive(Debug, Copy, Clone)] +pub enum SELinuxMode { + // ENFORCING constant to indicate SELinux is in enforcing mode + ENFORCING = 1, + // PERMISSIVE constant to indicate SELinux is in permissive mode + PERMISSIVE = 0, + // DISABLED constant to indicate SELinux is disabled + DISABLED = -1, +} + +impl From for SELinuxMode { + fn from(mode: i32) -> Self { + match mode { + 1 => SELinuxMode::ENFORCING, + 0 => SELinuxMode::PERMISSIVE, + -1 => SELinuxMode::DISABLED, + _ => SELinuxMode::DISABLED, + } + } +} + +impl From<&str> for SELinuxMode { + fn from(mode: &str) -> Self { + match mode { + "enforcing" => SELinuxMode::ENFORCING, + "permissive" => SELinuxMode::PERMISSIVE, + _ => SELinuxMode::DISABLED, + } + } +} + +impl fmt::Display for SELinuxMode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let s = match self { + SELinuxMode::ENFORCING => "enforcing", + SELinuxMode::PERMISSIVE => "permissive", + SELinuxMode::DISABLED => "disabled", + }; + write!(f, "{}", s) + } +} + +impl SELinuxMode { + pub fn as_bytes(&self) -> &[u8] { + match self { + SELinuxMode::ENFORCING => b"1", + SELinuxMode::PERMISSIVE => b"0", + SELinuxMode::DISABLED => b"-1", + } + } +} diff --git a/experiment/selinux/src/selinux.rs b/experiment/selinux/src/selinux.rs index ac2c91da2..f842dd0b1 100644 --- a/experiment/selinux/src/selinux.rs +++ b/experiment/selinux/src/selinux.rs @@ -1,305 +1,125 @@ -use crate::selinux_label::SELinuxLabel; +use crate::label::SELinuxLabel; +use crate::setting::SELinuxSetting; +use crate::SELinuxError; use nix::errno::Errno; use nix::sys::statfs; use nix::unistd::gettid; use std::collections::HashMap; use std::convert::From; -use std::fmt; use std::fs::{self, File, OpenOptions}; use std::io::{BufRead, BufReader, Read, Write}; use std::os::fd::{AsFd, AsRawFd}; use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicBool, Ordering}; - -#[derive(Debug, Copy, Clone)] -pub enum SELinuxMode { - // ENFORCING constant to indicate SELinux is in enforcing mode - ENFORCING = 1, - // PERMISSIVE constant to indicate SELinux is in permissive mode - PERMISSIVE = 0, - // DISABLED constant to indicate SELinux is disabled - DISABLED = -1, -} - -impl From for SELinuxMode { - fn from(mode: i32) -> Self { - match mode { - 1 => SELinuxMode::ENFORCING, - 0 => SELinuxMode::PERMISSIVE, - -1 => SELinuxMode::DISABLED, - _ => SELinuxMode::DISABLED, - } - } -} - -impl From<&str> for SELinuxMode { - fn from(mode: &str) -> Self { - match mode { - "enforcing" => SELinuxMode::ENFORCING, - "permissive" => SELinuxMode::PERMISSIVE, - _ => SELinuxMode::DISABLED, - } - } -} - -impl fmt::Display for SELinuxMode { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let s = match self { - SELinuxMode::ENFORCING => "enforcing", - SELinuxMode::PERMISSIVE => "permissive", - SELinuxMode::DISABLED => "disabled", - }; - write!(f, "{}", s) - } -} pub(crate) const ERR_EMPTY_PATH: &str = "empty path"; -const SELINUX_FS_MOUNT: &str = "/sys/fs/selinux"; +pub const DEFAULT_SELINUX_DIR: &str = "/etc/selinux/"; const CONTEXT_FILE: &str = "/usr/share/containers/selinux/contexts"; const SELINUX_TYPE_TAG: &str = "SELINUXTYPE"; -const SELINUX_TAG: &str = "SELINUX"; -const SELINUX_DIR: &str = "/etc/selinux/"; -const SELINUX_CONFIG: &str = "config"; - -#[derive(Debug, thiserror::Error)] -pub enum SELinuxError { - #[error("Failed to set file label for SELinux: {0}")] - SetFileLabel(String), - #[error("Failed to lset file label for SELinux: {0}")] - LSetFileLabel(String), - #[error("Failed to get file label for SELinux: {0}")] - FileLabel(String), - #[error("Failed to get lfile label for SELinux: {0}")] - LFileLabel(String), - #[error("Failed to call is_proc_handle for SELinux: {0}")] - IsProcHandle(String), - #[error("Failed to call read_con_fd for SELinux: {0}")] - ReadConFd(String), - #[error("Failed to call read_con for SELinux: {0}")] - ReadCon(String), - #[error("Failed to call write_con for SELinux: {0}")] - WriteCon(String), - #[error("Failed to find the index for a given class: {0}")] - ClassIndex(String), - #[error("Failed to call peer_label for SELinux: {0}")] - PeerLabel(String), - #[error("Failed to call open_context_file for SELinux: {0}")] - OpenContextFile(String), - #[error("Failed to set enforce mode of SELinux: {0}")] - SetEnforceMode(String), - #[error("Failed to read config file of SELinux: {0}")] - GetConfigKey(String), - #[error("Invalid format for SELinux label: {0}")] - InvalidSELinuxLabel(String), -} +// Linux >= 3.17 provides this +const THREAD_SELF_PREFIX: &str = "/proc/thread-self/attr"; -pub struct SELinux { - // for attr_path() - have_thread_self: AtomicBool, - attr_path_init_done: AtomicBool, +type SelinuxLabels = HashMap; - // for selinuxfs - selinuxfs_init_done: AtomicBool, - selinuxfs: Option, +pub struct SELinux<'a> { + setting: &'a SELinuxSetting, - // for policy_root() - policy_root_init_done: AtomicBool, - policy_root: Option, + policy_root: PathBuf, // for load_labels() - pub(crate) load_labels_init_done: AtomicBool, - pub(crate) labels: HashMap, + pub(crate) labels: SelinuxLabels, - // for read config and get config key - read_config_init_done: AtomicBool, - configs: HashMap, + have_thread: bool, - pub(crate) read_only_file_label: Option, + pub(crate) read_only_file_label: Option<&'a SELinuxLabel>, } -impl Default for SELinux { - fn default() -> Self { - SELinux::new() +impl<'a> SELinux<'a> { + pub fn try_default(setting: &'a SELinuxSetting) -> Result { + let have_thread_self = PathBuf::from(THREAD_SELF_PREFIX).is_dir(); + Self::new(CONTEXT_FILE, have_thread_self, setting) } -} -impl SELinux { - pub fn new() -> Self { - SELinux { - have_thread_self: AtomicBool::new(false), - attr_path_init_done: AtomicBool::new(false), - - selinuxfs_init_done: AtomicBool::new(false), - selinuxfs: None, - - policy_root_init_done: AtomicBool::new(false), - policy_root: None, - - load_labels_init_done: AtomicBool::new(false), + pub fn new>( + context_file: P, + have_thread: bool, + setting: &'a SELinuxSetting, + ) -> Result { + let mut selinux = SELinux { + setting, + policy_root: PathBuf::new(), labels: HashMap::new(), - - read_config_init_done: AtomicBool::new(false), - configs: HashMap::new(), - read_only_file_label: None, - } - } + have_thread, + }; - // This function returns policy_root. - // Directories under policy root has configuration files etc. - fn policy_root(&mut self) -> Option<&PathBuf> { - // Avoiding code conflicts and ensuring thread-safe execution once only. - if !self.policy_root_init_done.load(Ordering::SeqCst) { - let policy_root_path = Self::get_config_key(self, SELINUX_TYPE_TAG).unwrap_or_default(); - self.policy_root = Some(PathBuf::from(policy_root_path)); - self.policy_root_init_done.store(true, Ordering::SeqCst); - } - self.policy_root.as_ref() - } + selinux.policy_root = selinux.look_up_policy_root()?; + selinux.labels = selinux.load_labels(context_file)?; - // This function reads SELinux config file and returns the value with a specified key. - fn get_config_key(&mut self, target_key: &str) -> Result { - if !self.read_config_init_done.load(Ordering::SeqCst) { - let config_path = Path::new(SELINUX_DIR).join(SELINUX_CONFIG); - if let Ok(file) = File::open(config_path) { - let reader = BufReader::new(file); - for line in reader.lines().map_while(Result::ok) { - if line.is_empty() { - continue; - } - if (line.starts_with(';')) || (line.starts_with('#')) { - continue; - } - let fields: Vec<&str> = line.splitn(2, '=').collect(); - if fields.len() < 2 { - continue; - } - let key = fields[0].trim().to_string(); - let value = fields[1].trim().to_string(); - self.configs.insert(key, value); - } - } - self.read_config_init_done.store(true, Ordering::SeqCst); - } - self.configs - .get(target_key) - .cloned() - .filter(|s| !s.is_empty()) - .ok_or(SELinuxError::GetConfigKey(format!( - "can't find the target label in the config file: {}", - target_key - ))) + Ok(selinux) } // get_enabled returns whether SELinux is enabled or not. - pub fn get_enabled(&mut self) -> bool { - match Self::get_selinux_mountpoint(self) { - // If there is no SELinux mountpoint, SELinux is not enabled. - None => false, - Some(_) => match Self::current_label(self) { - Ok(con) => { - // Check whether label is "kernel" or not. - if con.user != "kernel" { - return true; - } - false + pub fn get_enabled(&self) -> bool { + match Self::current_label(self) { + Ok(con) => { + // Check whether label is "kernel" or not. + if con.user != "kernel" { + return true; } - Err(_) => false, - }, - } - } - - // verify_selinux_fs_mount verifies if the specified mount point is - // properly mounted as a writable SELinux filesystem. - fn verify_selinux_fs_mount>(mnt: P) -> bool { - let mnt = mnt.as_ref(); - loop { - match statfs::statfs(mnt) { - Ok(stat) => { - // In go-selinux, return false if it is not read-only, - // but selinux code in SELinuxProject return true even though it is read-only. - // https://github.com/SELinuxProject/selinux/blob/1f080ffd7ab24b0ad2b46f79db63d62c2ae2747c/libselinux/src/init.c#L44 - // Therefore, this function doesn't check whether it is read-only or not. - - // verify if the file is SELinux filesystem - return stat.filesystem_type() == statfs::SELINUX_MAGIC; - } - // check again if there is an issue while calling statfs - Err(Errno::EAGAIN) | Err(Errno::EINTR) => continue, - Err(_) => return false, + false } + Err(_) => false, } } - // check_line_include_selinux_fs_mount_point returns a next selinuxfs mount point found, - // if there is one, or None in case of EOF or error. - fn check_line_include_selinux_fs_mount_point(line: &str) -> Option { - if !line.contains(" - selinuxfs ") { - return None; - } - // Need to return the path like /sys/fs/selinux - // example: 28 24 0:25 / /sys/fs/selinux rw,relatime - selinuxfs selinuxfs rw - let m_pos = 5; - let fields: Vec<&str> = line.splitn(m_pos + 1, ' ').collect(); - if fields.len() < m_pos + 1 { - return None; - } - let mountpoint = fields[m_pos - 1].to_string(); - Some(PathBuf::from(mountpoint)) + // This function returns policy_root. + // Directories under policy root has configuration files etc. + fn look_up_policy_root(&self) -> Result { + // TODO: Remove `clone` + let policy_root_path = + PathBuf::from(DEFAULT_SELINUX_DIR).join(self.setting.get_config_key(SELINUX_TYPE_TAG)?); + Ok(policy_root_path) } - // find_selinux_fs finds the SELinux filesystem mount point. - fn find_selinux_fs() -> Option { - // fast path: check the default mount first - let selinux_fs_mount_path = PathBuf::from(SELINUX_FS_MOUNT); - if Self::verify_selinux_fs_mount(&selinux_fs_mount_path) { - return Some(selinux_fs_mount_path); - } - - // check if selinuxfs is available before going the slow path - let fs = fs::read_to_string("/proc/filesystems").unwrap_or_default(); - if !fs.contains("\tselinuxfs\n") { - return None; - } - - // slow path: try to find among the mounts - match File::open("/proc/self/mountinfo") { - Ok(file) => { - let reader = BufReader::new(file); - for line in reader.lines().map_while(Result::ok) { - if let Some(mnt) = Self::check_line_include_selinux_fs_mount_point(&line) { - if Self::verify_selinux_fs_mount(&mnt) { - return Some(mnt); - } - } + // This function loads context file and reads labels and stores it. + fn load_labels>( + &self, + context_file: P, + ) -> Result, SELinuxError> { + // The context file should have pairs of key and value like below. + // ---------- + // process = "system_u:system_r:container_t:s0" + // file = "system_u:object_r:container_file_t:s0" + // ---------- + let file = Self::open_context_file_impl(self, context_file) + .map_err(|e| SELinuxError::LoadLabels(e.to_string()))?; + let reader = BufReader::new(file); + Ok(reader + .lines() + .map_while(Result::ok) + .fold(HashMap::new(), |mut acc, line| { + let line = line.trim(); + if line.is_empty() { + return acc; } - } - Err(_) => return None, - } - None - } - - // This function returns the path to the mountpoint of an selinuxfs - // filesystem or an empty string if no mountpoint is found. Selinuxfs is - // a proc-like pseudo-filesystem that exposes the SELinux policy API to - // processes. The existence of an seliuxfs mount is used to determine - // whether SELinux is currently enabled or not. - pub fn get_selinux_mountpoint(&mut self) -> Option<&PathBuf> { - // Avoiding code conflicts and ensuring thread-safe execution once only. - if !self.selinuxfs_init_done.load(Ordering::SeqCst) { - self.selinuxfs = Self::find_selinux_fs(); - self.selinuxfs_init_done.store(true, Ordering::SeqCst); - } - self.selinuxfs.as_ref() + let fields: Vec<&str> = line.splitn(2, '=').collect(); + if fields.len() != 2 { + return acc; + } + let key = fields[0].trim().to_string(); + let value = fields[1].trim().to_string(); + if let Ok(value_label) = SELinuxLabel::try_from(value) { + acc.insert(key, value_label); + } + acc + })) } // classIndex returns the int index for an object class in the loaded policy, or an error. // For example, if a class is "file" or "dir", return the corresponding index for selinux. - pub fn class_index(&mut self, class: &str) -> Result { + pub fn class_index(&self, class: &str) -> Result { let permpath = format!("class/{}/index", class); - let mountpoint = Self::get_selinux_mountpoint(self) - .ok_or_else(|| SELinuxError::ClassIndex("SELinux mount point not found".to_string()))?; - let indexpath = mountpoint.join(permpath); + let indexpath = self.setting.selinuxfs.join(permpath); match fs::read_to_string(indexpath) { Ok(index_b) => match index_b.parse::() { @@ -312,73 +132,35 @@ impl SELinux { // This function attempts to open a selinux context file, and if it fails, it tries to open another file // under policy root's directory. - pub(crate) fn open_context_file(&mut self) -> Result { - match File::open(CONTEXT_FILE) { + // pub(crate) fn open_context_file>(&self) -> Result { + // self.open_context_file_impl(CONTEXT_FILE) + // } + + fn open_context_file_impl>(&self, path: P) -> Result { + match File::open(path.as_ref()) { Ok(file) => Ok(file), Err(_) => { - let policy_path = Self::policy_root(self).ok_or_else(|| { - SELinuxError::OpenContextFile("can't get policy root".to_string()) - })?; - let context_on_policy_root = policy_path.join("contexts").join("lxc_contexts"); - match File::open(context_on_policy_root) { + let context_on_policy_root = self.policy_root.join("contexts").join("lxc_contexts"); + match File::open(&context_on_policy_root) { Ok(file) => Ok(file), - Err(e) => Err(SELinuxError::OpenContextFile(e.to_string())), + Err(e) => Err(SELinuxError::OpenContextFile(format!( + "Failed to open context file({:?} and {:?}): {}", + &path.as_ref().as_os_str(), + context_on_policy_root.as_os_str(), + e + ))), } } } } - // This returns selinux enforce path by using selinux mountpoint. - // The enforce path dynamically changes SELinux mode at runtime, - // while the config file need OS to reboot after changing the config file. - fn selinux_enforce_path(&mut self) -> Option { - let selinux_mountpoint = Self::get_selinux_mountpoint(self); - selinux_mountpoint.map(|m| m.join("enforce")) - } - - // enforce_mode returns the current SELinux mode Enforcing, Permissive, Disabled - pub fn enforce_mode(&mut self) -> SELinuxMode { - let mode = match Self::selinux_enforce_path(self) { - Some(enforce_path) => match fs::read_to_string(enforce_path) { - Ok(content) => content.trim().parse::().unwrap_or(-1), - Err(_) => -1, - }, - None => -1, - }; - SELinuxMode::from(mode) - } - // is_mls_enabled checks if MLS is enabled. - pub fn is_mls_enabled(&mut self) -> bool { - if let Some(mountpoint) = Self::get_selinux_mountpoint(self) { - let mls_path = Path::new(&mountpoint).join("mls"); - match fs::read(mls_path) { - Ok(enabled_b) => return enabled_b == vec![b'1'], - Err(_) => return false, - } + pub fn is_mls_enabled(&self) -> bool { + let mls_path = self.setting.selinuxfs.join("mls"); + match fs::read(mls_path) { + Ok(enabled_b) => enabled_b == vec![b'1'], + Err(_) => false, } - false - } - - // This function updates the enforce mode of selinux. - // Disabled is not valid, since this needs to be set at boot time. - pub fn set_enforce_mode(&mut self, mode: SELinuxMode) -> Result<(), SELinuxError> { - let enforce_path = Self::selinux_enforce_path(self).ok_or_else(|| { - SELinuxError::SetEnforceMode("can't get selinux enforce path".to_string()) - })?; - fs::write(enforce_path, mode.to_string().as_bytes()) - .map_err(|e| SELinuxError::SetEnforceMode(e.to_string())) - } - - // This returns the systems default SELinux mode Enforcing, Permissive or Disabled. - // note this is just the default at boot time. - // enforce_mode function tells you the system current mode. - pub fn default_enforce_mode(&mut self) -> SELinuxMode { - SELinuxMode::from( - Self::get_config_key(self, SELINUX_TAG) - .unwrap_or_default() - .as_str(), - ) } // write_con writes a specified value to a given file path, handling SELinux context. @@ -391,10 +173,6 @@ impl SELinux { if path.as_os_str().is_empty() { return Err(SELinuxError::WriteCon(ERR_EMPTY_PATH.to_string())); } - if val.is_empty() && !Self::get_enabled(self) { - return Err(SELinuxError::WriteCon("SELinux is not enabled".to_string())); - } - let mut out = OpenOptions::new() .write(true) .create(false) @@ -461,16 +239,7 @@ impl SELinux { // attr_path determines the correct file path for accessing SELinux // attributes of a process or thread in a Linux environment. pub fn attr_path(&self, attr: &str) -> PathBuf { - // Linux >= 3.17 provides this - const THREAD_SELF_PREFIX: &str = "/proc/thread-self/attr"; - // Avoiding code conflicts and ensuring thread-safe execution once only. - if !self.attr_path_init_done.load(Ordering::SeqCst) { - let path = PathBuf::from(THREAD_SELF_PREFIX); - let is_dir = path.is_dir(); - self.have_thread_self.store(is_dir, Ordering::SeqCst); - self.attr_path_init_done.store(true, Ordering::SeqCst); - } - if self.have_thread_self.load(Ordering::SeqCst) { + if self.have_thread { return PathBuf::from(&format!("{}/{}", THREAD_SELF_PREFIX, attr)); } @@ -480,15 +249,13 @@ impl SELinux { #[cfg(test)] mod tests { - use crate::selinux::*; + use crate::{selinux::*, SELinuxSettingError}; use std::fs::File; use std::io::Write; use std::path::Path; - use std::str; use tempfile::NamedTempFile; - fn create_temp_file(content: &[u8], file_name: &str) { - let path = Path::new(file_name); + fn create_temp_file>(content: &[u8], path: P) { let mut file = File::create(path).expect("Failed to create file"); file.write_all(content).expect("Failed to write to file"); file.sync_all().expect("Failed to sync file"); @@ -513,8 +280,28 @@ mod tests { } #[test] - fn test_attr_path() { - let selinux = SELinux::new(); + fn test_attr_path() -> Result<(), SELinuxError> { + let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + create_temp_file( + b"SELINUX=enforcing\nSELINUXTYPE=targeted\n", + temp_dir.path().join("config"), + ); + create_temp_file(b"0", temp_dir.path().join("enforce")); + + let context_file = temp_dir.path().join("contexts"); + create_temp_file(b"process = \"system_u:system_r:container_t:s0\"\nfile = \"system_u:object_r:container_file_t:s0\"\n", context_file.as_path()); + + let setting_result = SELinuxSetting::new(temp_dir.path(), temp_dir.path()); + if matches!(setting_result, Err(SELinuxSettingError::NotInstalled)) { + // TODO: We should run the test even if SELinux is not installed. + println!("Skipping the test because SELinux is not installed"); + return Ok(()); + } + let setting = setting_result?; + + let selinux_result = SELinux::new(context_file.as_path(), true, &setting); + let mut selinux = selinux_result?; + // Test with "/proc/thread-self/attr" path (Linux >= 3.17) let attr = "bar"; let expected_name = &format!("/proc/thread-self/attr/{}", attr); @@ -523,13 +310,14 @@ mod tests { assert_eq!(expected_path, actual_path); // Test with not having "/proc/thread-self/attr" path by setting HAVE_THREAD_SELF as false - selinux.attr_path_init_done.store(true, Ordering::SeqCst); - selinux.have_thread_self.store(false, Ordering::SeqCst); + selinux = SELinux::new(context_file.as_path(), false, &setting)?; let thread_id = gettid(); let expected_name = &format!("/proc/self/task/{}/attr/{}", thread_id, attr); let expected_path = Path::new(expected_name); let actual_path = selinux.attr_path(attr); assert_eq!(expected_path, actual_path); + + Ok(()) } #[test] @@ -555,23 +343,4 @@ mod tests { } } } - - #[test] - fn test_check_line_include_selinux_fs_mount_point() { - let input_array = [ - "28 24 0:25 / /sys/fs/selinux rw,relatime - selinuxfs selinuxfs rw", - "28 24 0:25 /", - "28 24 0:25 / /sys/fs/selinux rw,relatime selinuxfs rw", - ]; - let expected_array = ["/sys/fs/selinux", "", ""]; - let succeeded_array = [true, false, false]; - - for (i, input) in input_array.iter().enumerate() { - let expected = PathBuf::from(expected_array[i]); - match SELinux::check_line_include_selinux_fs_mount_point(input) { - Some(output) => assert_eq!(expected, output), - None => assert!(!succeeded_array[i]), - } - } - } } diff --git a/experiment/selinux/src/setting.rs b/experiment/selinux/src/setting.rs new file mode 100644 index 000000000..81996f925 --- /dev/null +++ b/experiment/selinux/src/setting.rs @@ -0,0 +1,213 @@ +use std::collections::HashMap; +use std::fs::{self, File}; +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; + +use nix::errno::Errno; +use nix::sys::statfs; + +use crate::mode::SELinuxMode; +use crate::SELinuxSettingError; + +const SELINUX_CONFIG: &str = "config"; +const SELINUX_FS_MOUNT: &str = "/sys/fs/selinux"; + +pub struct SELinuxSetting { + // selinuxfs stores the path to the mountpoint of an selinuxfs + // filesystem or None if no mountpoint is found. selinuxfs is + // a proc-like pseudo-filesystem that exposes the SELinux policy API to + // processes. The existence of an seliuxfs mount is used to determine + // whether SELinux is currently enabled or not. + pub selinuxfs: PathBuf, + + // https://docs.rs/procfs/latest/procfs/process/struct.MountInfo.html + // https://github.com/opencontainers/selinux/blob/c79bd28d605a4ca93508e078dbe75a5ec995bf80/go-selinux/selinux_linux.go#L187 + // これ config を読むしかないのでは? + enforce_file: PathBuf, + configs: HashMap, +} + +impl SELinuxSetting { + pub fn try_default() -> Result { + let selinuxfs_result = Self::find_selinux_fs(); + if let Some(selinuxfs) = selinuxfs_result { + SELinuxSetting::new("/etc/selinux/", selinuxfs.as_path()) + } else { + Err(SELinuxSettingError::NotInstalled) + } + } + + pub fn new, P2: AsRef>( + selinux_dir: P1, + selinuxfs: P2, + ) -> Result { + let enforce_path = selinuxfs.as_ref().join("enforce"); + if !enforce_path.exists() { + return Err(SELinuxSettingError::EnforceFileNotFound(enforce_path)); + } + + // selinuxfs = selinuxmountpoint + Ok(SELinuxSetting { + selinuxfs: selinuxfs.as_ref().to_path_buf(), + enforce_file: enforce_path, + configs: Self::load_configs(selinux_dir)?, + }) + } + + fn load_configs>( + selinux_dir: P, + ) -> Result, SELinuxSettingError> { + let config_path = selinux_dir.as_ref().join(SELINUX_CONFIG); + let file = + File::open(config_path).map_err(|e| SELinuxSettingError::LoadConfig(e.to_string()))?; + let reader = BufReader::new(file); + Ok(reader + .lines() + .map_while(Result::ok) + .fold(HashMap::new(), |mut acc, line| { + let line = line.trim(); + if line.is_empty() { + return acc; + } + if line.starts_with(';') || line.starts_with('#') { + return acc; + } + let fields: Vec<&str> = line.splitn(2, '=').collect(); + if fields.len() < 2 { + return acc; + } + let key = fields[0].trim().to_string(); + let value = fields[1].trim().to_string(); + acc.insert(key, value); + acc + })) + } + + // find_selinux_fs finds the SELinux filesystem mount point. + fn find_selinux_fs() -> Option { + // fast path: check the default mount first + let selinux_fs_mount_path = PathBuf::from(SELINUX_FS_MOUNT); + if Self::verify_selinux_fs_mount(&selinux_fs_mount_path) { + return Some(selinux_fs_mount_path); + } + + // check if selinuxfs is available before going the slow path + let fs = fs::read_to_string("/proc/filesystems").unwrap_or_default(); + if !fs.contains("\tselinuxfs\n") { + return None; + } + + // slow path: try to find among the mounts + match File::open("/proc/self/mountinfo") { + Ok(file) => { + let reader = BufReader::new(file); + for line in reader.lines().map_while(Result::ok) { + if let Some(mnt) = Self::check_line_include_selinux_fs_mount_point(&line) { + if Self::verify_selinux_fs_mount(&mnt) { + return Some(mnt); + } + } + } + } + Err(_) => return None, + } + None + } + + // verify_selinux_fs_mount verifies if the specified mount point is + // properly mounted as a writable SELinux filesystem. + fn verify_selinux_fs_mount>(mnt: P) -> bool { + let mnt = mnt.as_ref(); + loop { + match statfs::statfs(mnt) { + Ok(stat) => { + // In go-selinux, return false if it is not read-only, + // but selinux code in SELinuxProject return true even though it is read-only. + // https://github.com/SELinuxProject/selinux/blob/1f080ffd7ab24b0ad2b46f79db63d62c2ae2747c/libselinux/src/init.c#L44 + // Therefore, this function doesn't check whether it is read-only or not. + + // verify if the file is SELinux filesystem + return stat.filesystem_type() == statfs::SELINUX_MAGIC; + } + // check again if there is an issue while calling statfs + Err(Errno::EAGAIN) | Err(Errno::EINTR) => continue, + Err(_) => return false, + } + } + } + + // check_line_include_selinux_fs_mount_point returns a next selinuxfs mount point found, + // if there is one, or None in case of EOF or error. + fn check_line_include_selinux_fs_mount_point(line: &str) -> Option { + if !line.contains(" - selinuxfs ") { + return None; + } + // Need to return the path like /sys/fs/selinux + // example: 28 24 0:25 / /sys/fs/selinux rw,relatime - selinuxfs selinuxfs rw + let m_pos = 5; + let fields: Vec<&str> = line.splitn(m_pos + 1, ' ').collect(); + if fields.len() < m_pos + 1 { + return None; + } + let mountpoint = fields[m_pos - 1].to_string(); + Some(PathBuf::from(mountpoint)) + } + + // This function reads SELinux config file and returns the value with a specified key. + pub fn get_config_key(&self, target_key: &str) -> Result { + return self + .configs + .get(target_key) + .cloned() + .filter(|s| !s.is_empty()) + .ok_or(SELinuxSettingError::GetConfigKey(format!( + "can't find the target label in the config file: {}", + target_key + ))); + } + + // enforce_mode returns the current SELinux mode Enforcing, Permissive, Disabled + pub fn enforce_mode(&self) -> Result { + match fs::read_to_string(&self.enforce_file) { + Ok(content) => content + .trim() + .parse::() + .map(SELinuxMode::from) + .map_err(|e| SELinuxSettingError::InvalidMode(e.to_string())), + Err(e) => Err(SELinuxSettingError::InvalidMode(e.to_string())), + } + } + + // This function updates the enforce mode of selinux. + // Disabled is not valid, since this needs to be set at boot time. + pub fn set_enforce_mode(&self, mode: SELinuxMode) -> Result<(), SELinuxSettingError> { + fs::write(&self.enforce_file, mode.as_bytes()) + .map_err(|e| SELinuxSettingError::SetEnforceMode(e.to_string())) + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::SELinuxSetting; + + #[test] + fn test_check_line_include_selinux_fs_mount_point() { + let input_array = [ + "28 24 0:25 / /sys/fs/selinux rw,relatime - selinuxfs selinuxfs rw", + "28 24 0:25 /", + "28 24 0:25 / /sys/fs/selinux rw,relatime selinuxfs rw", + ]; + let expected_array = ["/sys/fs/selinux", "", ""]; + let succeeded_array = [true, false, false]; + + for (i, input) in input_array.iter().enumerate() { + let expected = PathBuf::from(expected_array[i]); + match SELinuxSetting::check_line_include_selinux_fs_mount_point(input) { + Some(output) => assert_eq!(expected, output), + None => assert!(!succeeded_array[i]), + } + } + } +} diff --git a/experiment/selinux/test_file.txt b/experiment/selinux/test_file.txt new file mode 100644 index 000000000..e69de29bb