Skip to content
Open
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
15 changes: 13 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ 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]
assert_cmd = "2"
filetime = "0.2"
Expand Down
182 changes: 182 additions & 0 deletions src/find/matchers/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//! 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};

use std::error::Error;
#[cfg(target_os = "linux")]
use std::{
fs::File,
io::{stderr, BufRead, BufReader, Read, Write},
};

#[cfg(target_os = "linux")]
use super::glob::Pattern;
use super::{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 Ok(stat) = statfs(mnt) else { 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 Ok(fp) = File::open("/proc/filesystems") else {
return true; // Fail as if it exists
};

let reader = BufReader::new(fp);
for line in reader.lines().map_while(Result::ok) {
if line.contains(SELINUX_FS) {
return true;
}
}
false
}

/// Get SELinux mount point.
#[cfg(target_os = "linux")]
fn get_selinux_mnt() -> Option<String> {
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 Ok(fp) = File::open("/proc/mounts") else {
return None;
};

let reader = BufReader::new(fp);
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());
}
}
}
None
}

/// Check if SELinux is enforced.
#[cfg(target_os = "linux")]
fn get_selinux_enforced() -> Result<bool, Box<dyn Error>> {
let Some(mnt) = get_selinux_mnt() else {
return Ok(false);
};

let path = format!("{mnt}/enforce");
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 = buf.parse::<i32>()?;

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<Self, Box<dyn Error>> {
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, Box<dyn Error>> {
Ok(Self {})
}
}

impl Matcher for ContextMatcher {
#[cfg(target_os = "linux")]
fn matches(&self, path: &WalkEntry, _: &mut MatcherIO) -> bool {
let attr = match xattr::get(path.path(), XATTR_NAME_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;
}
};
self.pattern.matches(&selinux_ctx)
}

#[cfg(not(target_os = "linux"))]
fn matches(&self, _: &WalkEntry, _: &mut MatcherIO) -> bool {
false
}
}
9 changes: 9 additions & 0 deletions src/find/matchers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// https://opensource.org/licenses/MIT.

mod access;
mod context;
mod delete;
mod empty;
mod entry;
Expand Down Expand Up @@ -60,6 +61,7 @@ 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::{
Expand Down Expand Up @@ -774,6 +776,13 @@ fn build_matcher_tree(
i += 1;
Some(PermMatcher::new(args[i])?.into_box())
}
"-context" => {
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()),
Expand Down