From 2a3cd60fbdc53184342962cc7eac234d6c064744 Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Fri, 26 Dec 2025 18:53:44 +0000 Subject: [PATCH 1/6] Add SMACK support for ls utility --- Cargo.toml | 4 ++ src/uu/ls/Cargo.toml | 1 + src/uu/ls/src/ls.rs | 81 +++++++++++++++++----------- src/uucore/Cargo.toml | 1 + src/uucore/locales/en-US.ftl | 6 +++ src/uucore/src/lib/features.rs | 2 + src/uucore/src/lib/features/smack.rs | 74 +++++++++++++++++++++++++ src/uucore/src/lib/lib.rs | 3 ++ 8 files changed, 140 insertions(+), 32 deletions(-) create mode 100644 src/uucore/src/lib/features/smack.rs 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/src/ls.rs b/src/uu/ls/src/ls.rs index 85f01113914..edd5c70e20a 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,33 +3384,53 @@ 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!("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(); - 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!( + "getting security context of: {}: {}", + path.quote(), + 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 { + match std::fs::canonicalize(path) { + Ok(p) => p, + Err(_) => path.to_path_buf(), + } + } else { + path.to_path_buf() + }; + + match uucore::smack::get_smack_label_for_path(&target_path) { + Ok(label) => return Cow::Owned(label), + Err(_) => { + // No label or error getting label + return 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..8c2100d6e12 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -46,6 +46,12 @@ 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 } +# SMACK error messages +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 + # 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..62614104ea2 --- /dev/null +++ b/src/uucore/src/lib/features/smack.rs @@ -0,0 +1,74 @@ +// 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 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. +pub fn is_smack_enabled() -> bool { + 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)] From f3516d666d0f74980913296464e99fcd19c86568 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Fri, 26 Dec 2025 14:25:22 -0800 Subject: [PATCH 2/6] More idiomatic match statement Co-authored-by: Sylvestre Ledru --- src/uu/ls/src/ls.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index edd5c70e20a..a29239aa1fe 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -3418,10 +3418,7 @@ fn get_security_context<'a>( // For SMACK, use the path to get the label // If must_dereference is true, we follow the symlink let target_path = if must_dereference { - match std::fs::canonicalize(path) { - Ok(p) => p, - Err(_) => path.to_path_buf(), - } + std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) } else { path.to_path_buf() }; From b23e73b3a5f4b9690f731ef4c84342cbac600426 Mon Sep 17 00:00:00 2001 From: Chris Dryden Date: Fri, 26 Dec 2025 14:27:08 -0800 Subject: [PATCH 3/6] More idiomatic match statement for getting label Co-authored-by: Sylvestre Ledru --- src/uu/ls/src/ls.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index a29239aa1fe..b043a2de96e 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -3423,11 +3423,9 @@ fn get_security_context<'a>( path.to_path_buf() }; - match uucore::smack::get_smack_label_for_path(&target_path) { - Ok(label) => return Cow::Owned(label), - Err(_) => { - // No label or error getting label - return Cow::Borrowed(SUBSTITUTE_STRING); + return uucore::smack::get_smack_label_for_path(&target_path) + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed(SUBSTITUTE_STRING)); } } } From 4f7246a0f5c3162f5bae987025165f49e45637ee Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Fri, 26 Dec 2025 22:32:22 +0000 Subject: [PATCH 4/6] ls: fix smack formatting and cache is_smack_enabled result --- src/uu/ls/src/ls.rs | 10 ++++------ src/uucore/src/lib/features/smack.rs | 5 ++++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index b043a2de96e..453f27ab6da 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -3418,16 +3418,14 @@ fn get_security_context<'a>( // 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()) + 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)); - } - } + 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/src/lib/features/smack.rs b/src/uucore/src/lib/features/smack.rs index 62614104ea2..2a0250da5dd 100644 --- a/src/uucore/src/lib/features/smack.rs +++ b/src/uucore/src/lib/features/smack.rs @@ -8,6 +8,7 @@ use std::io; use std::path::Path; +use std::sync::OnceLock; use thiserror::Error; @@ -43,8 +44,10 @@ impl From for i32 { } /// Checks if SMACK is enabled by verifying smackfs is mounted. +/// The result is cached after the first call. pub fn is_smack_enabled() -> bool { - Path::new("/sys/fs/smackfs").exists() + 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. From d33be0a5d5065df638d3cb75c22ed589b3231a8c Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Sat, 27 Dec 2025 00:40:35 +0000 Subject: [PATCH 5/6] ls: use translate!() macro for SELinux warning messages --- src/uu/ls/locales/en-US.ftl | 4 ++++ src/uu/ls/src/ls.rs | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/uu/ls/locales/en-US.ftl b/src/uu/ls/locales/en-US.ftl index 03e1e2642c4..23ae41ab77b 100644 --- a/src/uu/ls/locales/en-US.ftl +++ b/src/uu/ls/locales/en-US.ftl @@ -124,3 +124,7 @@ 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} diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 453f27ab6da..3a7e8014e18 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -3389,7 +3389,13 @@ fn get_security_context<'a>( 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()); + 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), @@ -3400,9 +3406,12 @@ fn get_security_context<'a>( let res: String = String::from_utf8(context.to_vec()).unwrap_or_else(|e| { show_warning!( - "getting security context of: {}: {}", - path.quote(), - e.to_string() + "{}", + translate!( + "ls-warning-getting-security-context", + "path" => path.quote().to_string(), + "error" => e.to_string() + ) ); String::from_utf8_lossy(context).to_string() From 6004e2005bc89d731501d9130e8810f40f639c10 Mon Sep 17 00:00:00 2001 From: Christopher Dryden Date: Sun, 28 Dec 2025 02:34:00 +0000 Subject: [PATCH 6/6] Move SMACK locale strings to ls-specific locale file to avoid performance regression --- src/uu/ls/locales/en-US.ftl | 6 ++++++ src/uucore/locales/en-US.ftl | 5 ----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/uu/ls/locales/en-US.ftl b/src/uu/ls/locales/en-US.ftl index 23ae41ab77b..d5fc32b4f27 100644 --- a/src/uu/ls/locales/en-US.ftl +++ b/src/uu/ls/locales/en-US.ftl @@ -128,3 +128,9 @@ 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/uucore/locales/en-US.ftl b/src/uucore/locales/en-US.ftl index 8c2100d6e12..fa77f5270b3 100644 --- a/src/uucore/locales/en-US.ftl +++ b/src/uucore/locales/en-US.ftl @@ -46,11 +46,6 @@ 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 } -# SMACK error messages -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 # Safe traversal error messages safe-traversal-error-path-contains-null = path contains null byte