Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ feat_selinux = [
"id/selinux",
"ls/selinux",
"mkdir/selinux",
"stat/selinux",
"selinux",
"feat_require_selinux",
]
Expand Down
3 changes: 3 additions & 0 deletions src/uu/stat/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ clap = { workspace = true }
uucore = { workspace = true, features = ["entries", "libc", "fs", "fsext"] }
chrono = { workspace = true }

[features]
selinux = ["uucore/selinux"]

[[bin]]
name = "stat"
path = "src/main.rs"
22 changes: 21 additions & 1 deletion src/uu/stat/src/stat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -902,7 +902,27 @@ impl Stater {
// FIXME: blocksize differs on various platform
// See coreutils/gnulib/lib/stat-size.h ST_NBLOCKSIZE // spell-checker:disable-line
'B' => OutputType::Unsigned(512),

// SELinux security context string
'C' => {
#[cfg(feature = "selinux")]
{
if uucore::selinux::check_selinux_enabled().is_ok() {
match uucore::selinux::get_selinux_security_context(Path::new(file))
{
Ok(ctx) => OutputType::Str(ctx),
Err(_) => OutputType::Str(
"failed to get security context".to_string(),
),
}
} else {
OutputType::Str("unsupported on this system".to_string())
}
}
#[cfg(not(feature = "selinux"))]
{
OutputType::Str("unsupported for this operating system".to_string())
}
}
// device number in decimal
'd' => OutputType::Unsigned(meta.dev()),
// device number in hex
Expand Down
120 changes: 120 additions & 0 deletions src/uucore/src/lib/features/selinux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ use selinux::SecurityContext;
#[derive(Debug)]
pub enum Error {
SELinuxNotEnabled,
FileOpenFailure,
ContextRetrievalFailure,
ContextConversionFailure,
}

impl From<Error> for i32 {
fn from(error: Error) -> i32 {
match error {
Error::SELinuxNotEnabled => 1,
Error::FileOpenFailure => 2,
Error::ContextRetrievalFailure => 3,
Error::ContextConversionFailure => 4,
}
}
}

/// Checks if SELinux is enabled on the system.
Expand Down Expand Up @@ -109,6 +123,73 @@ pub fn set_selinux_security_context(path: &Path, context: Option<&String>) -> Re
}
}

/// Gets the SELinux security context for the given filesystem path.
///
/// Retrieves the security context of the specified filesystem path if SELinux is enabled
/// on the system.
///
/// # Arguments
///
/// * `path` - Filesystem path for which to retrieve the SELinux context.
///
/// # Returns
///
/// * `Ok(String)` - The SELinux context string if successfully retrieved. Returns an empty
/// string if no context was found.
/// * `Err(Error)` - An error variant indicating the type of failure:
/// - `Error::SELinuxNotEnabled` - SELinux is not enabled on the system.
/// - `Error::FileOpenFailure` - Failed to open the specified file.
/// - `Error::ContextRetrievalFailure` - Failed to retrieve the security context.
/// - `Error::ContextConversionFailure` - Failed to convert the security context to a string.
///
/// # Examples
///
/// ```
/// use std::path::Path;
/// use uucore::selinux::{get_selinux_security_context, Error};
///
/// // Get the SELinux context for a file
/// match get_selinux_security_context(Path::new("/path/to/file")) {
/// Ok(context) => {
/// if context.is_empty() {
/// println!("No SELinux context found for the file");
/// } else {
/// println!("SELinux context: {}", context);
/// }
/// },
/// Err(Error::SELinuxNotEnabled) => println!("SELinux is not enabled on this system"),
/// Err(Error::FileOpenFailure) => println!("Failed to open the file"),
/// Err(Error::ContextRetrievalFailure) => println!("Failed to retrieve the security context"),
/// Err(Error::ContextConversionFailure) => println!("Failed to convert the security context to a string"),
/// }
/// ```

pub fn get_selinux_security_context(path: &Path) -> Result<String, Error> {
if selinux::kernel_support() == selinux::KernelSupport::Unsupported {
return Err(Error::SELinuxNotEnabled);
}

let f = std::fs::File::open(path).map_err(|_| Error::FileOpenFailure)?;

// Get the security context of the file
let context = match SecurityContext::of_file(&f, false) {
Ok(Some(ctx)) => ctx,
Ok(None) => return Ok(String::new()), // No context found, return empty string
Err(_) => return Err(Error::ContextRetrievalFailure),
};

let context_c_string = context
.to_c_string()
.map_err(|_| Error::ContextConversionFailure)?;

if let Some(c_str) = context_c_string {
// Convert the C string to a Rust String
Ok(c_str.to_string_lossy().to_string())
} else {
Ok(String::new())
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -171,4 +252,43 @@ mod tests {
}
}
}

#[test]
fn test_get_selinux_security_context() {
let tmpfile = NamedTempFile::new().expect("Failed to create tempfile");
let path = tmpfile.path();

std::fs::write(path, b"test content").expect("Failed to write to tempfile");

let result = get_selinux_security_context(path);

if result.is_ok() {
println!("Retrieved SELinux context: {}", result.unwrap());
} else {
let err = result.unwrap_err();

// Valid error types
match err {
Error::SELinuxNotEnabled => assert!(true, "SELinux not supported"),
Error::ContextRetrievalFailure => assert!(true, "Context retrieval failure"),
Error::ContextConversionFailure => assert!(true, "Context conversion failure"),
Error::FileOpenFailure => {
panic!("File open failure occurred despite file being created")
}
}
}
}

#[test]
fn test_get_selinux_context_nonexistent_file() {
let path = Path::new("/nonexistent/file/that/does/not/exist");

let result = get_selinux_security_context(path);

assert!(result.is_err());
assert!(
matches!(result.unwrap_err(), Error::FileOpenFailure),
"Expected file open error for nonexistent file"
);
}
}
24 changes: 24 additions & 0 deletions tests/by-util/test_stat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,3 +490,27 @@ fn test_printf_invalid_directive() {
.fails_with_code(1)
.stderr_contains("'%9%': invalid directive");
}

#[test]
#[cfg(feature = "feat_selinux")]
fn test_stat_selinux() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.touch("f");
ts.ucmd()
.arg("--printf='%C'")
.arg("f")
.succeeds()
.no_stderr()
.stdout_contains("unconfined_u");
ts.ucmd()
.arg("--printf='%C'")
.arg("/bin/")
.succeeds()
.no_stderr()
.stdout_contains("system_u");
// Count that we have 4 fields
let result = ts.ucmd().arg("--printf='%C'").arg("/bin/").succeeds();
let s: Vec<_> = result.stdout_str().split(":").collect();
assert!(s.len() == 4);
}
Loading