Skip to content

Commit 219e87d

Browse files
authored
Merge pull request #7752 from sylvestre/stat-C
stat: add support for selinux
2 parents b78c343 + c7d39dd commit 219e87d

File tree

5 files changed

+169
-1
lines changed

5 files changed

+169
-1
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ feat_selinux = [
5151
"id/selinux",
5252
"ls/selinux",
5353
"mkdir/selinux",
54+
"stat/selinux",
5455
"selinux",
5556
"feat_require_selinux",
5657
]

src/uu/stat/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ clap = { workspace = true }
2222
uucore = { workspace = true, features = ["entries", "libc", "fs", "fsext"] }
2323
chrono = { workspace = true }
2424

25+
[features]
26+
selinux = ["uucore/selinux"]
27+
2528
[[bin]]
2629
name = "stat"
2730
path = "src/main.rs"

src/uu/stat/src/stat.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -902,7 +902,27 @@ impl Stater {
902902
// FIXME: blocksize differs on various platform
903903
// See coreutils/gnulib/lib/stat-size.h ST_NBLOCKSIZE // spell-checker:disable-line
904904
'B' => OutputType::Unsigned(512),
905-
905+
// SELinux security context string
906+
'C' => {
907+
#[cfg(feature = "selinux")]
908+
{
909+
if uucore::selinux::check_selinux_enabled().is_ok() {
910+
match uucore::selinux::get_selinux_security_context(Path::new(file))
911+
{
912+
Ok(ctx) => OutputType::Str(ctx),
913+
Err(_) => OutputType::Str(
914+
"failed to get security context".to_string(),
915+
),
916+
}
917+
} else {
918+
OutputType::Str("unsupported on this system".to_string())
919+
}
920+
}
921+
#[cfg(not(feature = "selinux"))]
922+
{
923+
OutputType::Str("unsupported for this operating system".to_string())
924+
}
925+
}
906926
// device number in decimal
907927
'd' => OutputType::Unsigned(meta.dev()),
908928
// device number in hex

src/uucore/src/lib/features/selinux.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ use selinux::SecurityContext;
1010
#[derive(Debug)]
1111
pub enum Error {
1212
SELinuxNotEnabled,
13+
FileOpenFailure,
14+
ContextRetrievalFailure,
15+
ContextConversionFailure,
16+
}
17+
18+
impl From<Error> for i32 {
19+
fn from(error: Error) -> i32 {
20+
match error {
21+
Error::SELinuxNotEnabled => 1,
22+
Error::FileOpenFailure => 2,
23+
Error::ContextRetrievalFailure => 3,
24+
Error::ContextConversionFailure => 4,
25+
}
26+
}
1327
}
1428

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

126+
/// Gets the SELinux security context for the given filesystem path.
127+
///
128+
/// Retrieves the security context of the specified filesystem path if SELinux is enabled
129+
/// on the system.
130+
///
131+
/// # Arguments
132+
///
133+
/// * `path` - Filesystem path for which to retrieve the SELinux context.
134+
///
135+
/// # Returns
136+
///
137+
/// * `Ok(String)` - The SELinux context string if successfully retrieved. Returns an empty
138+
/// string if no context was found.
139+
/// * `Err(Error)` - An error variant indicating the type of failure:
140+
/// - `Error::SELinuxNotEnabled` - SELinux is not enabled on the system.
141+
/// - `Error::FileOpenFailure` - Failed to open the specified file.
142+
/// - `Error::ContextRetrievalFailure` - Failed to retrieve the security context.
143+
/// - `Error::ContextConversionFailure` - Failed to convert the security context to a string.
144+
///
145+
/// # Examples
146+
///
147+
/// ```
148+
/// use std::path::Path;
149+
/// use uucore::selinux::{get_selinux_security_context, Error};
150+
///
151+
/// // Get the SELinux context for a file
152+
/// match get_selinux_security_context(Path::new("/path/to/file")) {
153+
/// Ok(context) => {
154+
/// if context.is_empty() {
155+
/// println!("No SELinux context found for the file");
156+
/// } else {
157+
/// println!("SELinux context: {}", context);
158+
/// }
159+
/// },
160+
/// Err(Error::SELinuxNotEnabled) => println!("SELinux is not enabled on this system"),
161+
/// Err(Error::FileOpenFailure) => println!("Failed to open the file"),
162+
/// Err(Error::ContextRetrievalFailure) => println!("Failed to retrieve the security context"),
163+
/// Err(Error::ContextConversionFailure) => println!("Failed to convert the security context to a string"),
164+
/// }
165+
/// ```
166+
167+
pub fn get_selinux_security_context(path: &Path) -> Result<String, Error> {
168+
if selinux::kernel_support() == selinux::KernelSupport::Unsupported {
169+
return Err(Error::SELinuxNotEnabled);
170+
}
171+
172+
let f = std::fs::File::open(path).map_err(|_| Error::FileOpenFailure)?;
173+
174+
// Get the security context of the file
175+
let context = match SecurityContext::of_file(&f, false) {
176+
Ok(Some(ctx)) => ctx,
177+
Ok(None) => return Ok(String::new()), // No context found, return empty string
178+
Err(_) => return Err(Error::ContextRetrievalFailure),
179+
};
180+
181+
let context_c_string = context
182+
.to_c_string()
183+
.map_err(|_| Error::ContextConversionFailure)?;
184+
185+
if let Some(c_str) = context_c_string {
186+
// Convert the C string to a Rust String
187+
Ok(c_str.to_string_lossy().to_string())
188+
} else {
189+
Ok(String::new())
190+
}
191+
}
192+
112193
#[cfg(test)]
113194
mod tests {
114195
use super::*;
@@ -171,4 +252,43 @@ mod tests {
171252
}
172253
}
173254
}
255+
256+
#[test]
257+
fn test_get_selinux_security_context() {
258+
let tmpfile = NamedTempFile::new().expect("Failed to create tempfile");
259+
let path = tmpfile.path();
260+
261+
std::fs::write(path, b"test content").expect("Failed to write to tempfile");
262+
263+
let result = get_selinux_security_context(path);
264+
265+
if result.is_ok() {
266+
println!("Retrieved SELinux context: {}", result.unwrap());
267+
} else {
268+
let err = result.unwrap_err();
269+
270+
// Valid error types
271+
match err {
272+
Error::SELinuxNotEnabled => assert!(true, "SELinux not supported"),
273+
Error::ContextRetrievalFailure => assert!(true, "Context retrieval failure"),
274+
Error::ContextConversionFailure => assert!(true, "Context conversion failure"),
275+
Error::FileOpenFailure => {
276+
panic!("File open failure occurred despite file being created")
277+
}
278+
}
279+
}
280+
}
281+
282+
#[test]
283+
fn test_get_selinux_context_nonexistent_file() {
284+
let path = Path::new("/nonexistent/file/that/does/not/exist");
285+
286+
let result = get_selinux_security_context(path);
287+
288+
assert!(result.is_err());
289+
assert!(
290+
matches!(result.unwrap_err(), Error::FileOpenFailure),
291+
"Expected file open error for nonexistent file"
292+
);
293+
}
174294
}

tests/by-util/test_stat.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,3 +490,27 @@ fn test_printf_invalid_directive() {
490490
.fails_with_code(1)
491491
.stderr_contains("'%9%': invalid directive");
492492
}
493+
494+
#[test]
495+
#[cfg(feature = "feat_selinux")]
496+
fn test_stat_selinux() {
497+
let ts = TestScenario::new(util_name!());
498+
let at = &ts.fixtures;
499+
at.touch("f");
500+
ts.ucmd()
501+
.arg("--printf='%C'")
502+
.arg("f")
503+
.succeeds()
504+
.no_stderr()
505+
.stdout_contains("unconfined_u");
506+
ts.ucmd()
507+
.arg("--printf='%C'")
508+
.arg("/bin/")
509+
.succeeds()
510+
.no_stderr()
511+
.stdout_contains("system_u");
512+
// Count that we have 4 fields
513+
let result = ts.ucmd().arg("--printf='%C'").arg("/bin/").succeeds();
514+
let s: Vec<_> = result.stdout_str().split(":").collect();
515+
assert!(s.len() == 4);
516+
}

0 commit comments

Comments
 (0)