From d4236dc893a0078796463884fe2652e62ab49213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Fri, 25 Apr 2025 22:22:43 +0800 Subject: [PATCH 1/6] Implement `-context` --- Cargo.lock | 15 +++++++++++-- Cargo.toml | 1 + src/find/matchers/context.rs | 43 ++++++++++++++++++++++++++++++++++++ src/find/matchers/mod.rs | 13 +++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 src/find/matchers/context.rs diff --git a/Cargo.lock b/Cargo.lock index 0d6a9c2e..4242015b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,6 +316,7 @@ dependencies = [ "tempfile", "uucore", "walkdir", + "xattr", ] [[package]] @@ -932,9 +933,9 @@ checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "syn" -version = "2.0.18" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -1246,6 +1247,16 @@ dependencies = [ "bitflags 2.4.1", ] +[[package]] +name = "xattr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" +dependencies = [ + "libc", + "rustix 1.0.0", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index e4c7056d..251910e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ onig = { version = "6.4", default-features = false } uucore = { version = "0.0.30", features = ["entries", "fs", "fsext", "mode"] } nix = { version = "0.30", features = ["fs", "user"] } argmax = "0.3.1" +xattr = "1.5.0" [dev-dependencies] assert_cmd = "2" diff --git a/src/find/matchers/context.rs b/src/find/matchers/context.rs new file mode 100644 index 00000000..fa2dcf4f --- /dev/null +++ b/src/find/matchers/context.rs @@ -0,0 +1,43 @@ +use std::{ + error::Error, + io::{stderr, Write}, +}; + +use super::{glob::Pattern, Matcher, MatcherIO, WalkEntry}; + +pub struct ContextMatcher { + pattern: Pattern, +} + +impl ContextMatcher { + pub fn new(pattern: &str) -> Result> { + let pattern = Pattern::new(pattern, false); + + Ok(Self { pattern }) + } +} + +impl Matcher for ContextMatcher { + fn matches(&self, path: &WalkEntry, _: &mut MatcherIO) -> bool { + let attr = match xattr::get(path.path(), "security.selinux") { + Ok(attr) => match attr { + Some(attr) => attr, + None => { + return false; + } + }, + Err(e) => { + writeln!(&mut stderr(), "Failed to get SELinux context: {e}").unwrap(); + return false; + } + }; + let selinux_ctx = match String::from_utf8(attr) { + Ok(selinux_ctx) => selinux_ctx, + Err(e) => { + writeln!(&mut stderr(), "Failed to convert SELinux context to UTF-8: {e}").unwrap(); + return false; + } + }; + return self.pattern.matches(&selinux_ctx); + } +} diff --git a/src/find/matchers/mod.rs b/src/find/matchers/mod.rs index ca2ed15e..c998667e 100644 --- a/src/find/matchers/mod.rs +++ b/src/find/matchers/mod.rs @@ -5,6 +5,7 @@ // https://opensource.org/licenses/MIT. mod access; +mod context; mod delete; mod empty; mod entry; @@ -60,8 +61,10 @@ use self::type_matcher::{TypeMatcher, XtypeMatcher}; use self::user::{NoUserMatcher, UserMatcher}; use ::regex::Regex; use chrono::{DateTime, Datelike, NaiveDateTime, Utc}; +use context::ContextMatcher; use fs::FileSystemMatcher; use ls::Ls; +use std::fs::exists; use std::{ error::Error, fs::{File, Metadata}, @@ -774,6 +777,16 @@ fn build_matcher_tree( i += 1; Some(PermMatcher::new(args[i])?.into_box()) } + "-context" => { + if !exists("/sys/fs/selinux/enforce").unwrap_or(false) { + return Err(From::from(format!("SELinux is not enabled"))); + } + if i >= args.len() - 1 { + return Err(From::from(format!("missing argument to {}", args[i]))); + } + i += 1; + Some(ContextMatcher::new(args[i])?.into_box()) + } "-prune" => Some(PruneMatcher::new().into_box()), "-quit" => Some(QuitMatcher.into_box()), "-writable" => Some(AccessMatcher::Writable.into_box()), From 8b7fb6dfc6912a0682ff9a5f0ac5a42e28d572aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Sat, 26 Apr 2025 20:09:39 +0800 Subject: [PATCH 2/6] context: pure rust code to check SELinux --- src/find/matchers/context.rs | 157 ++++++++++++++++++++++++++++++++++- src/find/matchers/mod.rs | 4 - 2 files changed, 154 insertions(+), 7 deletions(-) diff --git a/src/find/matchers/context.rs b/src/find/matchers/context.rs index fa2dcf4f..2b7528c6 100644 --- a/src/find/matchers/context.rs +++ b/src/find/matchers/context.rs @@ -1,25 +1,167 @@ +//! SELinux context matcher +//! +//! This matcher will match files based on their +//! SELinux context, only available on Linux. + +#[cfg(target_os = "linux")] +use nix::{libc::SELINUX_MAGIC, sys::statvfs::FsFlags}; +#[cfg(target_os = "linux")] use std::{ error::Error, - io::{stderr, Write}, + fs::File, + io::{stderr, BufRead, BufReader, Read, Write}, }; +#[cfg(target_os = "linux")] use super::{glob::Pattern, Matcher, MatcherIO, WalkEntry}; +#[cfg(target_os = "linux")] +const XATTR_NAME_SELINUX: &str = "security.selinux"; +#[cfg(target_os = "linux")] +const SELINUX_FS: &str = "selinuxfs"; +#[cfg(target_os = "linux")] +const SELINUX_MNT: &str = "/sys/fs/selinux"; +#[cfg(target_os = "linux")] +const OLD_SELINUX_MNT: &str = "/selinux"; + +/// Verify if SELinux mount point exists and is writable. +/// +/// This function will return true if the SELinux mount point +/// exists and is writable, false otherwise. +#[cfg(target_os = "linux")] +fn verify_selinux_mnt(mnt: &str) -> bool { + use nix::sys::statfs::{statfs, FsType}; + use nix::sys::statvfs::statvfs; + + let stat = match statfs(mnt) { + Ok(stat) => stat, + Err(_) => return false, + }; + + if stat.filesystem_type() == FsType(SELINUX_MAGIC) { + match statvfs(mnt) { + Ok(stat) => { + if stat.flags().contains(FsFlags::ST_RDONLY) { + return false; + } + return true; + } + Err(_) => return false, + } + } + false +} + +/// Check if SELinux filesystem exists. +/// +/// This function will try to open the `/proc/filesystems` file and +/// check if the SELinux filesystem is listed. +#[cfg(target_os = "linux")] +fn selinuxfs_exists() -> bool { + let fp = match File::open("/proc/filesystems") { + Ok(f) => f, + Err(_) => return true, // Fail as if it exists + }; + + let reader = BufReader::new(fp); + for line in reader.lines() { + if let Ok(line) = line { + if line.contains(SELINUX_FS) { + return true; + } + } + } + false +} + +/// Get SELinux mount point. +#[cfg(target_os = "linux")] +fn get_selinux_mnt() -> Option { + if verify_selinux_mnt(SELINUX_MNT) { + return Some(SELINUX_MNT.to_string()); + } + + if verify_selinux_mnt(OLD_SELINUX_MNT) { + return Some(OLD_SELINUX_MNT.to_string()); + } + + if !selinuxfs_exists() { + return None; + } + + let fp = match File::open("/proc/mounts") { + Ok(f) => f, + Err(_) => return None, + }; + + let reader = BufReader::new(fp); + for line in reader.lines() { + if let Ok(line) = line { + let mut parts = line.splitn(3, ' '); + if let (Some(_), Some(mnt), Some(fs_type)) = (parts.next(), parts.next(), parts.next()) + { + if fs_type.starts_with(SELINUX_FS) { + if verify_selinux_mnt(mnt) { + return Some(mnt.to_string()); + } + } + } + } + } + None +} + +/// Check if SELinux is enforced. +#[cfg(target_os = "linux")] +fn get_selinux_enforced() -> Result> { + let mnt = match get_selinux_mnt() { + Some(mnt) => mnt, + None => return Ok(false), + }; + + let path = format!("{mnt}/enforce"); + let mut fd = match File::open(path) { + Ok(f) => f, + Err(_) => return Ok(false), + }; + + let mut buf = String::with_capacity(20); + if fd.read_to_string(&mut buf).is_err() { + return Ok(false); + } + let enforce = i8::from_str_radix(&buf, 10)?; + + Ok(enforce != 0) +} + +/// Matcher for SELinux context. pub struct ContextMatcher { + #[cfg(target_os = "linux")] pattern: Pattern, } impl ContextMatcher { + #[cfg(target_os = "linux")] pub fn new(pattern: &str) -> Result> { + if !get_selinux_enforced()? { + return Err(From::from("SELinux is not enabled")); + } + let pattern = Pattern::new(pattern, false); Ok(Self { pattern }) } + + #[cfg(not(target_os = "linux"))] + pub fn new(_pattern: &str) -> Result> { + Self {} + } } impl Matcher for ContextMatcher { + #[cfg(target_os = "linux")] fn matches(&self, path: &WalkEntry, _: &mut MatcherIO) -> bool { - let attr = match xattr::get(path.path(), "security.selinux") { + let attr = match xattr::get(path.path(), XATTR_NAME_SELINUX) { Ok(attr) => match attr { Some(attr) => attr, None => { @@ -34,10 +176,19 @@ impl Matcher for ContextMatcher { let selinux_ctx = match String::from_utf8(attr) { Ok(selinux_ctx) => selinux_ctx, Err(e) => { - writeln!(&mut stderr(), "Failed to convert SELinux context to UTF-8: {e}").unwrap(); + writeln!( + &mut stderr(), + "Failed to convert SELinux context to UTF-8: {e}" + ) + .unwrap(); return false; } }; return self.pattern.matches(&selinux_ctx); } + + #[cfg(not(target_os = "linux"))] + fn matches(&self, _: &WalkEntry, _: &mut MatcherIO) -> bool { + false + } } diff --git a/src/find/matchers/mod.rs b/src/find/matchers/mod.rs index c998667e..ce8d6510 100644 --- a/src/find/matchers/mod.rs +++ b/src/find/matchers/mod.rs @@ -64,7 +64,6 @@ use chrono::{DateTime, Datelike, NaiveDateTime, Utc}; use context::ContextMatcher; use fs::FileSystemMatcher; use ls::Ls; -use std::fs::exists; use std::{ error::Error, fs::{File, Metadata}, @@ -778,9 +777,6 @@ fn build_matcher_tree( Some(PermMatcher::new(args[i])?.into_box()) } "-context" => { - if !exists("/sys/fs/selinux/enforce").unwrap_or(false) { - return Err(From::from(format!("SELinux is not enabled"))); - } if i >= args.len() - 1 { return Err(From::from(format!("missing argument to {}", args[i]))); } From 8af2ea5d0a7355849a6cff2bf314909a6b64dbd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Sat, 26 Apr 2025 20:24:13 +0800 Subject: [PATCH 3/6] context: fix cross build --- src/find/matchers/context.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/find/matchers/context.rs b/src/find/matchers/context.rs index 2b7528c6..c6e7fb9c 100644 --- a/src/find/matchers/context.rs +++ b/src/find/matchers/context.rs @@ -5,15 +5,17 @@ #[cfg(target_os = "linux")] use nix::{libc::SELINUX_MAGIC, sys::statvfs::FsFlags}; + +use std::error::Error; #[cfg(target_os = "linux")] use std::{ - error::Error, fs::File, io::{stderr, BufRead, BufReader, Read, Write}, }; #[cfg(target_os = "linux")] -use super::{glob::Pattern, Matcher, MatcherIO, WalkEntry}; +use super::glob::Pattern; +use super::{Matcher, MatcherIO, WalkEntry}; #[cfg(target_os = "linux")] const XATTR_NAME_SELINUX: &str = "security.selinux"; From caeb359dd41e527b8d77cf298337293c987eb9b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Sat, 26 Apr 2025 20:25:35 +0800 Subject: [PATCH 4/6] context: fix cross build --- src/find/matchers/context.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/find/matchers/context.rs b/src/find/matchers/context.rs index c6e7fb9c..d3639e17 100644 --- a/src/find/matchers/context.rs +++ b/src/find/matchers/context.rs @@ -156,7 +156,7 @@ impl ContextMatcher { #[cfg(not(target_os = "linux"))] pub fn new(_pattern: &str) -> Result> { - Self {} + Ok(Self {}) } } From d18a13c9f8a8507904b28acfb9a95b64d3dc6c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Sat, 26 Apr 2025 20:39:46 +0800 Subject: [PATCH 5/6] clippy: fix clippy --- src/find/matchers/context.rs | 52 +++++++++++++----------------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/src/find/matchers/context.rs b/src/find/matchers/context.rs index d3639e17..6a0d84c5 100644 --- a/src/find/matchers/context.rs +++ b/src/find/matchers/context.rs @@ -35,10 +35,7 @@ fn verify_selinux_mnt(mnt: &str) -> bool { use nix::sys::statfs::{statfs, FsType}; use nix::sys::statvfs::statvfs; - let stat = match statfs(mnt) { - Ok(stat) => stat, - Err(_) => return false, - }; + let Ok(stat) = statfs(mnt) else { return false }; if stat.filesystem_type() == FsType(SELINUX_MAGIC) { match statvfs(mnt) { @@ -60,17 +57,14 @@ fn verify_selinux_mnt(mnt: &str) -> bool { /// check if the SELinux filesystem is listed. #[cfg(target_os = "linux")] fn selinuxfs_exists() -> bool { - let fp = match File::open("/proc/filesystems") { - Ok(f) => f, - Err(_) => return true, // Fail as if it exists + let Ok(fp) = File::open("/proc/filesystems") else { + return true; // Fail as if it exists }; let reader = BufReader::new(fp); - for line in reader.lines() { - if let Ok(line) = line { - if line.contains(SELINUX_FS) { - return true; - } + for line in reader.lines().map_while(Result::ok) { + if line.contains(SELINUX_FS) { + return true; } } false @@ -91,22 +85,16 @@ fn get_selinux_mnt() -> Option { return None; } - let fp = match File::open("/proc/mounts") { - Ok(f) => f, - Err(_) => return None, + let Ok(fp) = File::open("/proc/mounts") else { + return None; }; let reader = BufReader::new(fp); - for line in reader.lines() { - if let Ok(line) = line { - let mut parts = line.splitn(3, ' '); - if let (Some(_), Some(mnt), Some(fs_type)) = (parts.next(), parts.next(), parts.next()) - { - if fs_type.starts_with(SELINUX_FS) { - if verify_selinux_mnt(mnt) { - return Some(mnt.to_string()); - } - } + for line in reader.lines().map_while(Result::ok) { + let mut parts = line.splitn(3, ' '); + if let (Some(_), Some(mnt), Some(fs_type)) = (parts.next(), parts.next(), parts.next()) { + if fs_type.starts_with(SELINUX_FS) && verify_selinux_mnt(mnt) { + return Some(mnt.to_string()); } } } @@ -116,22 +104,20 @@ fn get_selinux_mnt() -> Option { /// Check if SELinux is enforced. #[cfg(target_os = "linux")] fn get_selinux_enforced() -> Result> { - let mnt = match get_selinux_mnt() { - Some(mnt) => mnt, - None => return Ok(false), + let Some(mnt) = get_selinux_mnt() else { + return Ok(false); }; let path = format!("{mnt}/enforce"); - let mut fd = match File::open(path) { - Ok(f) => f, - Err(_) => return Ok(false), + let Ok(mut fd) = File::open(path) else { + return Ok(false); }; let mut buf = String::with_capacity(20); if fd.read_to_string(&mut buf).is_err() { return Ok(false); } - let enforce = i8::from_str_radix(&buf, 10)?; + let enforce = buf.parse::()?; Ok(enforce != 0) } @@ -186,7 +172,7 @@ impl Matcher for ContextMatcher { return false; } }; - return self.pattern.matches(&selinux_ctx); + self.pattern.matches(&selinux_ctx) } #[cfg(not(target_os = "linux"))] From d4e084f8147967873d736ec5710e7d6bf700af0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= Date: Sat, 26 Apr 2025 20:51:31 +0800 Subject: [PATCH 6/6] xattr: fix windows compile --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 251910e2..1f591c2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,8 @@ onig = { version = "6.4", default-features = false } uucore = { version = "0.0.30", features = ["entries", "fs", "fsext", "mode"] } nix = { version = "0.30", features = ["fs", "user"] } argmax = "0.3.1" + +[target.'cfg(target_os = "linux")'.dependencies] xattr = "1.5.0" [dev-dependencies]