diff --git a/Cargo.toml b/Cargo.toml index b8b6f48fc6a..d6737d16d08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,10 @@ feat_selinux = [ "selinux", "stat/selinux", ] +# "feat_smack" == enable support for SMACK Security Context (by using `--features feat_smack`) +# NOTE: +# * Running a uutils compiled with `feat_smack` requires a SMACK enabled Kernel at run time. +feat_smack = ["ls/smack"] ## ## feature sets ## (common/core and Tier1) feature sets diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index e6cd07fa4a9..a96d0910899 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -60,3 +60,4 @@ harness = false [features] feat_selinux = ["selinux", "uucore/selinux"] +smack = ["uucore/smack"] diff --git a/src/uu/ls/locales/en-US.ftl b/src/uu/ls/locales/en-US.ftl index 03e1e2642c4..d5fc32b4f27 100644 --- a/src/uu/ls/locales/en-US.ftl +++ b/src/uu/ls/locales/en-US.ftl @@ -124,3 +124,13 @@ ls-invalid-columns-width = ignoring invalid width in environment variable COLUMN ls-invalid-ignore-pattern = Invalid pattern for ignore: {$pattern} ls-invalid-hide-pattern = Invalid pattern for hide: {$pattern} ls-total = total {$size} + +# Security context warnings +ls-warning-failed-to-get-security-context = failed to get security context of: {$path} +ls-warning-getting-security-context = getting security context of: {$path}: {$error} + +# SMACK error messages (used by uucore::smack when called from ls) +smack-error-not-enabled = SMACK is not enabled on this system +smack-error-label-retrieval-failure = failed to get SMACK label: { $error } +smack-error-label-set-failure = failed to set SMACK label to '{ $context }': { $error } +smack-error-no-label-set = no SMACK label set diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 85f01113914..3a7e8014e18 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -365,7 +365,10 @@ pub struct Config { time_format_recent: String, // Time format for recent dates time_format_older: Option, // Time format for older dates (optional, if not present, time_format_recent is used) context: bool, + #[cfg(all(feature = "selinux", target_os = "linux"))] selinux_supported: bool, + #[cfg(all(feature = "smack", target_os = "linux"))] + smack_supported: bool, group_directories_first: bool, line_ending: LineEnding, dired: bool, @@ -1157,16 +1160,10 @@ impl Config { time_format_recent, time_format_older, context, - selinux_supported: { - #[cfg(all(feature = "selinux", target_os = "linux"))] - { - uucore::selinux::is_selinux_enabled() - } - #[cfg(not(all(feature = "selinux", target_os = "linux")))] - { - false - } - }, + #[cfg(all(feature = "selinux", target_os = "linux"))] + selinux_supported: uucore::selinux::is_selinux_enabled(), + #[cfg(all(feature = "smack", target_os = "linux"))] + smack_supported: uucore::smack::is_smack_enabled(), group_directories_first: options.get_flag(options::GROUP_DIRECTORIES_FIRST), line_ending: LineEnding::from_zero_flag(options.get_flag(options::ZERO)), dired, @@ -3387,37 +3384,59 @@ fn get_security_context<'a>( } } + #[cfg(all(feature = "selinux", target_os = "linux"))] if config.selinux_supported { - #[cfg(all(feature = "selinux", target_os = "linux"))] - { - match selinux::SecurityContext::of_path(path, must_dereference, false) { - Err(_r) => { - // TODO: show the actual reason why it failed - show_warning!("failed to get security context of: {}", path.quote()); - return Cow::Borrowed(SUBSTITUTE_STRING); - } - Ok(None) => return Cow::Borrowed(SUBSTITUTE_STRING), - Ok(Some(context)) => { - let context = context.as_bytes(); + match selinux::SecurityContext::of_path(path, must_dereference, false) { + Err(_r) => { + // TODO: show the actual reason why it failed + show_warning!( + "{}", + translate!( + "ls-warning-failed-to-get-security-context", + "path" => path.quote().to_string() + ) + ); + return Cow::Borrowed(SUBSTITUTE_STRING); + } + Ok(None) => return Cow::Borrowed(SUBSTITUTE_STRING), + Ok(Some(context)) => { + let context = context.as_bytes(); - let context = context.strip_suffix(&[0]).unwrap_or(context); + let context = context.strip_suffix(&[0]).unwrap_or(context); - let res: String = String::from_utf8(context.to_vec()).unwrap_or_else(|e| { - show_warning!( - "getting security context of: {}: {}", - path.quote(), - e.to_string() - ); + let res: String = String::from_utf8(context.to_vec()).unwrap_or_else(|e| { + show_warning!( + "{}", + translate!( + "ls-warning-getting-security-context", + "path" => path.quote().to_string(), + "error" => e.to_string() + ) + ); - String::from_utf8_lossy(context).to_string() - }); + String::from_utf8_lossy(context).to_string() + }); - return Cow::Owned(res); - } + return Cow::Owned(res); } } } + #[cfg(all(feature = "smack", target_os = "linux"))] + if config.smack_supported { + // For SMACK, use the path to get the label + // If must_dereference is true, we follow the symlink + let target_path = if must_dereference { + std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) + } else { + path.to_path_buf() + }; + + return uucore::smack::get_smack_label_for_path(&target_path) + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed(SUBSTITUTE_STRING)); + } + Cow::Borrowed(SUBSTITUTE_STRING) } diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index d30b88eeb49..0362dc09793 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -162,6 +162,7 @@ ranges = [] ringbuffer = [] safe-traversal = ["libc"] selinux = ["dep:selinux"] +smack = ["xattr"] signals = [] sum = [ "digest", diff --git a/src/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl index b5930062998..fa77f5270b3 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -46,6 +46,7 @@ selinux-error-context-retrieval-failure = failed to retrieve the security contex selinux-error-context-set-failure = failed to set default file creation context to '{ $context }': { $error } selinux-error-context-conversion-failure = failed to set default file creation context to '{ $context }': { $error } + # Safe traversal error messages safe-traversal-error-path-contains-null = path contains null byte safe-traversal-error-open-failed = failed to open { $path }: { $source } diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index 548f7f2bc95..e56968c50fa 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -85,6 +85,8 @@ pub mod hardware; pub mod selinux; #[cfg(all(unix, not(target_os = "fuchsia"), feature = "signals"))] pub mod signals; +#[cfg(all(target_os = "linux", feature = "smack"))] +pub mod smack; #[cfg(feature = "feat_systemd_logind")] pub mod systemd_logind; #[cfg(all( diff --git a/src/uucore/src/lib/features/smack.rs b/src/uucore/src/lib/features/smack.rs new file mode 100644 index 00000000000..2a0250da5dd --- /dev/null +++ b/src/uucore/src/lib/features/smack.rs @@ -0,0 +1,77 @@ +// This file is part of the uutils uucore package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore smackfs +//! SMACK (Simplified Mandatory Access Control Kernel) support + +use std::io; +use std::path::Path; +use std::sync::OnceLock; + +use thiserror::Error; + +use crate::error::{UError, strip_errno}; +use crate::translate; + +#[derive(Debug, Error)] +pub enum SmackError { + #[error("{}", translate!("smack-error-not-enabled"))] + SmackNotEnabled, + + #[error("{}", translate!("smack-error-label-retrieval-failure", "error" => strip_errno(.0)))] + LabelRetrievalFailure(io::Error), + + #[error("{}", translate!("smack-error-label-set-failure", "context" => .0.clone(), "error" => strip_errno(.1)))] + LabelSetFailure(String, io::Error), +} + +impl UError for SmackError { + fn code(&self) -> i32 { + match self { + Self::SmackNotEnabled => 1, + Self::LabelRetrievalFailure(_) => 2, + Self::LabelSetFailure(_, _) => 3, + } + } +} + +impl From for i32 { + fn from(error: SmackError) -> Self { + error.code() + } +} + +/// Checks if SMACK is enabled by verifying smackfs is mounted. +/// The result is cached after the first call. +pub fn is_smack_enabled() -> bool { + static SMACK_ENABLED: OnceLock = OnceLock::new(); + *SMACK_ENABLED.get_or_init(|| Path::new("/sys/fs/smackfs").exists()) +} + +/// Gets the SMACK label for a filesystem path via xattr. +pub fn get_smack_label_for_path(path: &Path) -> Result { + if !is_smack_enabled() { + return Err(SmackError::SmackNotEnabled); + } + + match xattr::get(path, "security.SMACK64") { + Ok(Some(value)) => Ok(String::from_utf8_lossy(&value).trim().to_string()), + Ok(None) => Err(SmackError::LabelRetrievalFailure(io::Error::new( + io::ErrorKind::NotFound, + translate!("smack-error-no-label-set"), + ))), + Err(e) => Err(SmackError::LabelRetrievalFailure(e)), + } +} + +/// Sets the SMACK label for a filesystem path via xattr. +pub fn set_smack_label_for_path(path: &Path, label: &str) -> Result<(), SmackError> { + if !is_smack_enabled() { + return Err(SmackError::SmackNotEnabled); + } + + xattr::set(path, "security.SMACK64", label.as_bytes()) + .map_err(|e| SmackError::LabelSetFailure(label.to_string(), e)) +} diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index e930ea30f97..7931a69205e 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -125,6 +125,9 @@ pub use crate::features::fsxattr; #[cfg(all(target_os = "linux", feature = "selinux"))] pub use crate::features::selinux; +#[cfg(all(target_os = "linux", feature = "smack"))] +pub use crate::features::smack; + //## core functions #[cfg(unix)]