From 83a515e4c3aab8c529526db059d5df606fffa872 Mon Sep 17 00:00:00 2001 From: Koutheir Attouchi Date: Fri, 6 Aug 2021 09:52:34 -0400 Subject: [PATCH] chcon: reduce the number of unsafe blocks. --- Cargo.lock | 10 +- Cargo.toml | 2 +- src/uu/chcon/Cargo.toml | 2 +- src/uu/chcon/src/chcon.rs | 699 +++++++------------------------------ src/uu/chcon/src/errors.rs | 71 ++++ src/uu/chcon/src/fts.rs | 193 ++++++++++ src/uu/id/Cargo.toml | 2 +- src/uu/id/src/id.rs | 2 - 8 files changed, 407 insertions(+), 574 deletions(-) create mode 100644 src/uu/chcon/src/errors.rs create mode 100644 src/uu/chcon/src/fts.rs diff --git a/Cargo.lock b/Cargo.lock index 9fe45b941..8cf7cddcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -713,9 +713,9 @@ checksum = "31a7a908b8f32538a2143e59a6e4e2508988832d5d4d6f7c156b3cbc762643a5" [[package]] name = "filetime" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" dependencies = [ "cfg-if 1.0.0", "libc", @@ -1631,9 +1631,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "selinux" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd525eeb189eb26c8471463186bba87644e3d8a9c7ae392adaf9ec45ede574bc" +checksum = "1aa2f705dd871c2eb90888bb2d44b13218b34f5c7318c3971df62f799d0143eb" dependencies = [ "bitflags", "libc", @@ -2033,7 +2033,7 @@ dependencies = [ "clap", "fts-sys", "libc", - "selinux-sys", + "selinux", "thiserror", "uucore", "uucore_procs", diff --git a/Cargo.toml b/Cargo.toml index df60206ca..abf6a42de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -241,7 +241,7 @@ clap = { version = "2.33", features = ["wrap_help"] } lazy_static = { version="1.3" } textwrap = { version="=0.11.0", features=["term_size"] } # !maint: [2020-05-10; rivy] unstable crate using undocumented features; pinned currently, will review uucore = { version=">=0.0.9", package="uucore", path="src/uucore" } -selinux = { version="0.1.3", optional = true } +selinux = { version="0.2.1", optional = true } # * uutils uu_test = { optional=true, version="0.0.7", package="uu_test", path="src/uu/test" } # diff --git a/src/uu/chcon/Cargo.toml b/src/uu/chcon/Cargo.toml index 7e7f82c3b..56fbef9ba 100644 --- a/src/uu/chcon/Cargo.toml +++ b/src/uu/chcon/Cargo.toml @@ -17,7 +17,7 @@ path = "src/chcon.rs" clap = { version = "2.33", features = ["wrap_help"] } uucore = { version = ">=0.0.9", package="uucore", path="../../uucore", features=["entries", "fs", "perms"] } uucore_procs = { version = ">=0.0.6", package="uucore_procs", path="../../uucore_procs" } -selinux-sys = { version = "0.5" } +selinux = { version = "0.2" } fts-sys = { version = "0.2" } thiserror = { version = "1.0" } libc = { version = "0.2" } diff --git a/src/uu/chcon/src/chcon.rs b/src/uu/chcon/src/chcon.rs index 0e8b4b440..2a5efba46 100644 --- a/src/uu/chcon/src/chcon.rs +++ b/src/uu/chcon/src/chcon.rs @@ -5,14 +5,18 @@ use uucore::{executable, show_error, show_usage_error, show_warning}; use clap::{App, Arg}; +use selinux::{OpaqueSecurityContext, SecurityContext}; +use std::borrow::Cow; use std::ffi::{CStr, CString, OsStr, OsString}; -use std::fmt::Write; -use std::os::raw::{c_char, c_int}; +use std::os::raw::c_int; use std::path::{Path, PathBuf}; -use std::{fs, io, ptr, slice}; +use std::{fs, io}; -type Result = std::result::Result; +mod errors; +mod fts; + +use errors::*; static VERSION: &str = env!("CARGO_PKG_VERSION"); static ABOUT: &str = "Change the SELinux security context of each FILE to CONTEXT. \n\ @@ -81,15 +85,16 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let context = match &options.mode { CommandLineMode::ReferenceBased { reference } => { - let result = selinux::FileContext::new(reference, true) - .and_then(|r| { - if r.is_empty() { - Err(io::Error::from_raw_os_error(libc::ENODATA)) - } else { - Ok(r) - } - }) - .map_err(|r| Error::io1("Getting security context", reference, r)); + let result = match SecurityContext::of_path(reference, true, false) { + Ok(Some(context)) => Ok(context), + + Ok(None) => { + let err = io::Error::from_raw_os_error(libc::ENODATA); + Err(Error::from_io1("Getting security context", reference, err)) + } + + Err(r) => Err(Error::from_selinux("Getting security context", r)), + }; match result { Err(r) => { @@ -102,33 +107,24 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } CommandLineMode::ContextBased { context } => { - match selinux::SecurityContext::security_check_context(context) - .map_err(|r| Error::io1("Checking security context", context, r)) - { - Err(r) => { - show_error!("{}.", report_full_error(&r)); - return libc::EXIT_FAILURE; - } + let c_context = match os_str_to_c_string(context) { + Ok(context) => context, - Ok(Some(false)) => { + Err(_r) => { show_error!("Invalid security context '{}'.", context.to_string_lossy()); return libc::EXIT_FAILURE; } - - Ok(Some(true)) | Ok(None) => {} - } - - let c_context = if let Ok(value) = os_str_to_c_string(context) { - value - } else { - show_error!("Invalid security context '{}'.", context.to_string_lossy()); - return libc::EXIT_FAILURE; }; - SELinuxSecurityContext::String(c_context) + if SecurityContext::from_c_str(&c_context, false).check() == Some(false) { + show_error!("Invalid security context '{}'.", context.to_string_lossy()); + return libc::EXIT_FAILURE; + } + + SELinuxSecurityContext::String(Some(c_context)) } - CommandLineMode::Custom { .. } => SELinuxSecurityContext::default(), + CommandLineMode::Custom { .. } => SELinuxSecurityContext::String(None), }; let root_dev_ino = if options.preserve_root && options.recursive_mode.is_recursive() { @@ -282,65 +278,6 @@ pub fn uu_app() -> App<'static, 'static> { .arg(Arg::with_name("FILE").multiple(true).min_values(1)) } -fn report_full_error(mut err: &dyn std::error::Error) -> String { - let mut desc = String::with_capacity(256); - write!(&mut desc, "{}", err).unwrap(); - while let Some(source) = err.source() { - err = source; - write!(&mut desc, ". {}", err).unwrap(); - } - desc -} - -#[derive(thiserror::Error, Debug)] -enum Error { - #[error("No context is specified")] - MissingContext, - - #[error("No files are specified")] - MissingFiles, - - #[error("{0}")] - ArgumentsMismatch(String), - - #[error(transparent)] - CommandLine(#[from] clap::Error), - - #[error("{operation} failed")] - Io { - operation: &'static str, - source: io::Error, - }, - - #[error("{operation} failed on '{}'", .operand1.to_string_lossy())] - Io1 { - operation: &'static str, - operand1: OsString, - source: io::Error, - }, -} - -impl Error { - fn io1(operation: &'static str, operand1: impl Into, source: io::Error) -> Self { - Self::Io1 { - operation, - operand1: operand1.into(), - source, - } - } - - #[cfg(unix)] - fn io1_c_str(operation: &'static str, operand1: &CStr, source: io::Error) -> Self { - if operand1.to_bytes().is_empty() { - Self::Io { operation, source } - } else { - use std::os::unix::ffi::OsStrExt; - - Self::io1(operation, OsStr::from_bytes(operand1.to_bytes()), source) - } - } -} - #[derive(Debug)] struct Options { verbose: bool, @@ -500,39 +437,29 @@ fn process_files( root_dev_ino: Option<(libc::ino_t, libc::dev_t)>, ) -> Vec { let fts_options = options.recursive_mode.fts_open_options(); - let mut fts = match fts::FTS::new(options.files.iter(), fts_options, None) { + let mut fts = match fts::FTS::new(options.files.iter(), fts_options) { Ok(fts) => fts, - Err(source) => { - return vec![Error::Io { - operation: "fts_open()", - source, - }] - } + Err(err) => return vec![err], }; - let mut results = vec![]; - + let mut errors = Vec::default(); loop { match fts.read_next_entry() { Ok(true) => { if let Err(err) = process_file(options, context, &mut fts, root_dev_ino) { - results.push(err); + errors.push(err); } } Ok(false) => break, - Err(source) => { - results.push(Error::Io { - operation: "fts_read()", - source, - }); - + Err(err) => { + errors.push(err); break; } } } - results + errors } fn process_file( @@ -541,46 +468,40 @@ fn process_file( fts: &mut fts::FTS, root_dev_ino: Option<(libc::ino_t, libc::dev_t)>, ) -> Result<()> { - let entry = fts.last_entry_mut().unwrap(); + let mut entry = fts.last_entry_ref().unwrap(); - let file_full_name = if entry.fts_path.is_null() { - None - } else { - let fts_path_size = usize::from(entry.fts_pathlen).saturating_add(1); - - // SAFETY: `entry.fts_path` is a non-null pointer that is assumed to be valid. - let bytes = unsafe { slice::from_raw_parts(entry.fts_path.cast(), fts_path_size) }; - CStr::from_bytes_with_nul(bytes).ok() - } - .ok_or_else(|| Error::Io { - operation: "File name validation", - source: io::ErrorKind::InvalidInput.into(), + let file_full_name = entry.path().map(PathBuf::from).ok_or_else(|| { + Error::from_io("File name validation", io::ErrorKind::InvalidInput.into()) })?; - let fts_access_path = ptr_to_c_str(entry.fts_accpath) - .map_err(|r| Error::io1_c_str("File name validation", file_full_name, r))?; + let fts_access_path = entry.access_path().ok_or_else(|| { + let err = io::ErrorKind::InvalidInput.into(); + Error::from_io1("File name validation", &file_full_name, err) + })?; - let err = |s, k: io::ErrorKind| Error::io1_c_str(s, file_full_name, k.into()); + let err = |s, k: io::ErrorKind| Error::from_io1(s, &file_full_name, k.into()); let fts_err = |s| { - let r = io::Error::from_raw_os_error(entry.fts_errno); - Err(Error::io1_c_str(s, file_full_name, r)) + let r = io::Error::from_raw_os_error(entry.errno()); + Err(Error::from_io1(s, &file_full_name, r)) }; // SAFETY: If `entry.fts_statp` is not null, then is is assumed to be valid. - let file_dev_ino = unsafe { entry.fts_statp.as_ref() } - .map(|stat| (stat.st_ino, stat.st_dev)) - .ok_or_else(|| err("Getting meta data", io::ErrorKind::InvalidInput))?; + let file_dev_ino = if let Some(stat) = entry.stat() { + (stat.st_ino, stat.st_dev) + } else { + return Err(err("Getting meta data", io::ErrorKind::InvalidInput)); + }; let mut result = Ok(()); - match c_int::from(entry.fts_info) { + match entry.flags() { fts_sys::FTS_D => { if options.recursive_mode.is_recursive() { if root_dev_ino_check(root_dev_ino, file_dev_ino) { // This happens e.g., with "chcon -R --preserve-root ... /" // and with "chcon -RH --preserve-root ... symlink-to-root". - root_dev_ino_warn(file_full_name); + root_dev_ino_warn(&file_full_name); // Tell fts not to traverse into this hierarchy. let _ignored = fts.set(fts_sys::FTS_SKIP); @@ -607,8 +528,8 @@ fn process_file( // that modify permissions, it is possible that the file in question is accessible when // control reaches this point. So, if this is the first time we've seen the FTS_NS for // this file, tell fts_read to stat it "again". - if entry.fts_level == 0 && entry.fts_number == 0 { - entry.fts_number = 1; + if entry.level() == 0 && entry.number() == 0 { + entry.set_number(1); let _ignored = fts.set(fts_sys::FTS_AGAIN); return Ok(()); } @@ -621,8 +542,8 @@ fn process_file( fts_sys::FTS_DNR => result = fts_err("Reading directory"), fts_sys::FTS_DC => { - if cycle_warning_required(options.recursive_mode.fts_open_options(), entry) { - emit_cycle_warning(file_full_name); + if cycle_warning_required(options.recursive_mode.fts_open_options(), &entry) { + emit_cycle_warning(&file_full_name); return Err(err("Reading cyclic directory", io::ErrorKind::InvalidData)); } } @@ -630,11 +551,11 @@ fn process_file( _ => {} } - if c_int::from(entry.fts_info) == fts_sys::FTS_DP + if entry.flags() == fts_sys::FTS_DP && result.is_ok() && root_dev_ino_check(root_dev_ino, file_dev_ino) { - root_dev_ino_warn(file_full_name); + root_dev_ino_warn(&file_full_name); result = Err(err("Modifying root path", io::ErrorKind::PermissionDenied)); } @@ -656,24 +577,10 @@ fn process_file( result } -fn set_file_security_context( - path: &Path, - context: *const c_char, - follow_symbolic_links: bool, -) -> Result<()> { - let mut file_context = selinux::FileContext::from_ptr(context as *mut c_char); - if file_context.context.is_null() { - Err(io::Error::from(io::ErrorKind::InvalidInput)) - } else { - file_context.set_for_file(path, follow_symbolic_links) - } - .map_err(|r| Error::io1("Setting security context", path, r)) -} - fn change_file_context( options: &Options, context: &SELinuxSecurityContext, - file: &CStr, + path: &Path, ) -> Result<()> { match &options.mode { CommandLineMode::Custom { @@ -682,91 +589,88 @@ fn change_file_context( the_type, range, } => { - let path = PathBuf::from(c_str_to_os_string(file)); - let file_context = selinux::FileContext::new(&path, options.affect_symlink_referent) - .map_err(|r| Error::io1("Getting security context", &path, r))?; + let err0 = || -> Result<()> { + // If the file doesn't have a context, and we're not setting all of the context + // components, there isn't really an obvious default. Thus, we just give up. + let op = "Applying partial security context to unlabeled file"; + let err = io::ErrorKind::InvalidInput.into(); + Err(Error::from_io1(op, path, err)) + }; - // If the file doesn't have a context, and we're not setting all of the context - // components, there isn't really an obvious default. Thus, we just give up. - if file_context.is_empty() { - return Err(Error::io1( - "Applying partial security context to unlabeled file", - path, - io::ErrorKind::InvalidInput.into(), - )); - } + let file_context = + match SecurityContext::of_path(path, options.affect_symlink_referent, false) { + Ok(Some(context)) => context, - let mut se_context = selinux::SecurityContext::new(file_context.as_ptr()) - .map_err(|r| Error::io1("Creating security context", &path, r))?; + Ok(None) => return err0(), + Err(r) => return Err(Error::from_selinux("Getting security context", r)), + }; - if let Some(user) = user { - se_context - .set_user(user) - .map_err(|r| Error::io1("Setting security context user", &path, r))?; - } + let c_file_context = match file_context.to_c_string() { + Ok(Some(context)) => context, - if let Some(role) = role { - se_context - .set_role(role) - .map_err(|r| Error::io1("Setting security context role", &path, r))?; - } + Ok(None) => return err0(), + Err(r) => return Err(Error::from_selinux("Getting security context", r)), + }; - if let Some(the_type) = the_type { - se_context - .set_type(the_type) - .map_err(|r| Error::io1("Setting security context type", &path, r))?; - } + let se_context = + OpaqueSecurityContext::from_c_str(c_file_context.as_ref()).map_err(|_r| { + let err = io::ErrorKind::InvalidInput.into(); + Error::from_io1("Creating security context", path, err) + })?; - if let Some(range) = range { - se_context - .set_range(range) - .map_err(|r| Error::io1("Setting security context range", &path, r))?; + type SetValueProc = fn(&OpaqueSecurityContext, &CStr) -> selinux::errors::Result<()>; + + let list: &[(&Option, SetValueProc)] = &[ + (user, OpaqueSecurityContext::set_user), + (role, OpaqueSecurityContext::set_role), + (the_type, OpaqueSecurityContext::set_type), + (range, OpaqueSecurityContext::set_range), + ]; + + for (new_value, set_value_proc) in list { + if let Some(new_value) = new_value { + let c_new_value = os_str_to_c_string(new_value).map_err(|_r| { + let err = io::ErrorKind::InvalidInput.into(); + Error::from_io1("Creating security context", path, err) + })?; + + set_value_proc(&se_context, &c_new_value) + .map_err(|r| Error::from_selinux("Setting security context user", r))?; + } } let context_string = se_context - .str_bytes() - .map_err(|r| Error::io1("Getting security context", &path, r))?; + .to_c_string() + .map_err(|r| Error::from_selinux("Getting security context", r))?; - if !file_context.is_empty() && file_context.as_bytes() == context_string { + if c_file_context.as_ref().to_bytes() == context_string.as_ref().to_bytes() { Ok(()) // Nothing to change. } else { - set_file_security_context( - &path, - context_string.as_ptr().cast(), - options.affect_symlink_referent, - ) + SecurityContext::from_c_str(&context_string, false) + .set_for_path(path, options.affect_symlink_referent, false) + .map_err(|r| Error::from_selinux("Setting security context", r)) } } CommandLineMode::ReferenceBased { .. } | CommandLineMode::ContextBased { .. } => { - let path = PathBuf::from(c_str_to_os_string(file)); - let ctx_ptr = context.as_ptr() as *mut c_char; - set_file_security_context(&path, ctx_ptr, options.affect_symlink_referent) + if let Some(c_context) = context.to_c_string()? { + SecurityContext::from_c_str(c_context.as_ref(), false) + .set_for_path(path, options.affect_symlink_referent, false) + .map_err(|r| Error::from_selinux("Setting security context", r)) + } else { + let err = io::ErrorKind::InvalidInput.into(); + Err(Error::from_io1("Setting security context", path, err)) + } } } } #[cfg(unix)] -fn c_str_to_os_string(s: &CStr) -> OsString { - use std::os::unix::ffi::OsStringExt; - - OsString::from_vec(s.to_bytes().to_vec()) -} - -#[cfg(unix)] -pub(crate) fn os_str_to_c_string(s: &OsStr) -> io::Result { +pub(crate) fn os_str_to_c_string(s: &OsStr) -> Result { use std::os::unix::ffi::OsStrExt; - CString::new(s.as_bytes()).map_err(|_r| io::ErrorKind::InvalidInput.into()) -} - -/// SAFETY: -/// - If `p` is not null, then it is assumed to be a valid null-terminated C string. -/// - The returned `CStr` must not live more than the data pointed-to by `p`. -fn ptr_to_c_str<'s>(p: *const c_char) -> io::Result<&'s CStr> { - ptr::NonNull::new(p as *mut c_char) - .map(|p| unsafe { CStr::from_ptr(p.as_ptr()) }) - .ok_or_else(|| io::ErrorKind::InvalidInput.into()) + CString::new(s.as_bytes()) + .map_err(|_r| Error::from_io("CString::new()", io::ErrorKind::InvalidInput.into())) } /// Call `lstat()` to get the device and inode numbers for `/`. @@ -776,7 +680,7 @@ fn get_root_dev_ino() -> Result<(libc::ino_t, libc::dev_t)> { fs::symlink_metadata("/") .map(|md| (md.ino(), md.dev())) - .map_err(|r| Error::io1("std::fs::symlink_metadata", "/", r)) + .map_err(|r| Error::from_io1("std::fs::symlink_metadata", "/", r)) } fn root_dev_ino_check( @@ -786,8 +690,8 @@ fn root_dev_ino_check( root_dev_ino.map_or(false, |root_dev_ino| root_dev_ino == dir_dev_ino) } -fn root_dev_ino_warn(dir_name: &CStr) { - if dir_name.to_bytes() == b"/" { +fn root_dev_ino_warn(dir_name: &Path) { + if dir_name.as_os_str() == "/" { show_warning!( "It is dangerous to operate recursively on '/'. \ Use --{} to override this failsafe.", @@ -810,370 +714,37 @@ fn root_dev_ino_warn(dir_name: &CStr) { // However, when invoked with "-P -R", it deserves a warning. // The fts_options parameter records the options that control this aspect of fts's behavior, // so test that. -fn cycle_warning_required(fts_options: c_int, entry: &fts_sys::FTSENT) -> bool { +fn cycle_warning_required(fts_options: c_int, entry: &fts::EntryRef) -> bool { // When dereferencing no symlinks, or when dereferencing only those listed on the command line // and we're not processing a command-line argument, then a cycle is a serious problem. ((fts_options & fts_sys::FTS_PHYSICAL) != 0) - && (((fts_options & fts_sys::FTS_COMFOLLOW) == 0) || entry.fts_level != 0) + && (((fts_options & fts_sys::FTS_COMFOLLOW) == 0) || entry.level() != 0) } -fn emit_cycle_warning(file_name: &CStr) { +fn emit_cycle_warning(file_name: &Path) { show_warning!( "Circular directory structure.\n\ This almost certainly means that you have a corrupted file system.\n\ NOTIFY YOUR SYSTEM MANAGER.\n\ The following directory is part of the cycle '{}'.", - file_name.to_string_lossy() + file_name.display() ) } #[derive(Debug)] -enum SELinuxSecurityContext { - File(selinux::FileContext), - String(CString), +enum SELinuxSecurityContext<'t> { + File(SecurityContext<'t>), + String(Option), } -impl Default for SELinuxSecurityContext { - fn default() -> Self { - Self::String(CString::default()) - } -} - -impl SELinuxSecurityContext { - #[cfg(unix)] - fn as_ptr(&self) -> *const c_char { +impl<'t> SELinuxSecurityContext<'t> { + fn to_c_string(&self) -> Result>> { match self { - SELinuxSecurityContext::File(context) => context.as_ptr(), - SELinuxSecurityContext::String(context) => context.to_bytes_with_nul().as_ptr().cast(), - } - } -} - -mod fts { - use std::ffi::{CStr, CString, OsStr}; - use std::os::raw::c_int; - use std::{io, iter, ptr}; - - use super::os_str_to_c_string; - - pub(crate) type FTSOpenCallBack = unsafe extern "C" fn( - arg1: *mut *const fts_sys::FTSENT, - arg2: *mut *const fts_sys::FTSENT, - ) -> c_int; - - #[derive(Debug)] - pub(crate) struct FTS { - fts: ptr::NonNull, - entry: *mut fts_sys::FTSENT, - } - - impl FTS { - pub(crate) fn new( - paths: I, - options: c_int, - compar: Option, - ) -> io::Result - where - I: IntoIterator, - I::Item: AsRef, - { - let files_paths = paths - .into_iter() - .map(|s| os_str_to_c_string(s.as_ref())) - .collect::>>()?; - - if files_paths.is_empty() { - return Err(io::ErrorKind::InvalidInput.into()); - } - - let path_argv = files_paths - .iter() - .map(CString::as_ref) - .map(CStr::as_ptr) - .chain(iter::once(ptr::null())) - .collect::>(); - - // SAFETY: We assume calling fts_open() is safe: - // - `path_argv` is an array holding at least one path, and null-terminated. - let r = unsafe { fts_sys::fts_open(path_argv.as_ptr().cast(), options, compar) }; - let fts = ptr::NonNull::new(r).ok_or_else(io::Error::last_os_error)?; - - Ok(Self { - fts, - entry: ptr::null_mut(), - }) - } - - pub(crate) fn read_next_entry(&mut self) -> io::Result { - // SAFETY: We assume calling fts_read() is safe with a non-null `fts` - // pointer assumed to be valid. - self.entry = unsafe { fts_sys::fts_read(self.fts.as_ptr()) }; - if self.entry.is_null() { - let r = io::Error::last_os_error(); - if let Some(0) = r.raw_os_error() { - Ok(false) - } else { - Err(r) - } - } else { - Ok(true) - } - } - - pub(crate) fn last_entry_mut(&mut self) -> Option<&mut fts_sys::FTSENT> { - // SAFETY: If `self.entry` is not null, then is is assumed to be valid. - unsafe { self.entry.as_mut() } - } - - pub(crate) fn set(&mut self, instr: c_int) -> io::Result<()> { - let fts = self.fts.as_ptr(); - - let entry = self - .last_entry_mut() - .ok_or_else(|| io::Error::from(io::ErrorKind::UnexpectedEof))?; - - // SAFETY: We assume calling fts_set() is safe with non-null `fts` - // and `entry` pointers assumed to be valid. - if unsafe { fts_sys::fts_set(fts, entry, instr) } == -1 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } - } - } - - impl Drop for FTS { - fn drop(&mut self) { - // SAFETY: We assume calling fts_close() is safe with a non-null `fts` - // pointer assumed to be valid. - unsafe { fts_sys::fts_close(self.fts.as_ptr()) }; - } - } -} - -mod selinux { - use std::ffi::OsStr; - use std::os::raw::c_char; - use std::path::Path; - use std::{io, ptr, slice}; - - use super::os_str_to_c_string; - - #[derive(Debug)] - pub(crate) struct SecurityContext(ptr::NonNull); - - impl SecurityContext { - pub(crate) fn new(context_str: *const c_char) -> io::Result { - if context_str.is_null() { - Err(io::ErrorKind::InvalidInput.into()) - } else { - // SAFETY: We assume calling context_new() is safe with - // a non-null `context_str` pointer assumed to be valid. - let p = unsafe { selinux_sys::context_new(context_str) }; - ptr::NonNull::new(p) - .ok_or_else(io::Error::last_os_error) - .map(Self) - } - } - - pub(crate) fn is_selinux_enabled() -> bool { - // SAFETY: We assume calling is_selinux_enabled() is always safe. - unsafe { selinux_sys::is_selinux_enabled() != 0 } - } - - pub(crate) fn security_check_context(context: &OsStr) -> io::Result> { - let c_context = os_str_to_c_string(context)?; - - // SAFETY: We assume calling security_check_context() is safe with - // a non-null `context` pointer assumed to be valid. - if unsafe { selinux_sys::security_check_context(c_context.as_ptr()) } == 0 { - Ok(Some(true)) - } else if Self::is_selinux_enabled() { - Ok(Some(false)) - } else { - Ok(None) - } - } - - pub(crate) fn str_bytes(&self) -> io::Result<&[u8]> { - // SAFETY: We assume calling context_str() is safe with - // a non-null `context` pointer assumed to be valid. - let p = unsafe { selinux_sys::context_str(self.0.as_ptr()) }; - if p.is_null() { - Err(io::ErrorKind::InvalidInput.into()) - } else { - let len = unsafe { libc::strlen(p.cast()) }.saturating_add(1); - Ok(unsafe { slice::from_raw_parts(p.cast(), len) }) - } - } - - pub(crate) fn set_user(&mut self, user: &OsStr) -> io::Result<()> { - let c_user = os_str_to_c_string(user)?; - - // SAFETY: We assume calling context_user_set() is safe with non-null - // `context` and `user` pointers assumed to be valid. - if unsafe { selinux_sys::context_user_set(self.0.as_ptr(), c_user.as_ptr()) } == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } - } - - pub(crate) fn set_role(&mut self, role: &OsStr) -> io::Result<()> { - let c_role = os_str_to_c_string(role)?; - - // SAFETY: We assume calling context_role_set() is safe with non-null - // `context` and `role` pointers assumed to be valid. - if unsafe { selinux_sys::context_role_set(self.0.as_ptr(), c_role.as_ptr()) } == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } - } - - pub(crate) fn set_type(&mut self, the_type: &OsStr) -> io::Result<()> { - let c_type = os_str_to_c_string(the_type)?; - - // SAFETY: We assume calling context_type_set() is safe with non-null - // `context` and `the_type` pointers assumed to be valid. - if unsafe { selinux_sys::context_type_set(self.0.as_ptr(), c_type.as_ptr()) } == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } - } - - pub(crate) fn set_range(&mut self, range: &OsStr) -> io::Result<()> { - let c_range = os_str_to_c_string(range)?; - - // SAFETY: We assume calling context_range_set() is safe with non-null - // `context` and `range` pointers assumed to be valid. - if unsafe { selinux_sys::context_range_set(self.0.as_ptr(), c_range.as_ptr()) } == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } - } - } - - impl Drop for SecurityContext { - fn drop(&mut self) { - // SAFETY: We assume calling context_free() is safe with - // a non-null `context` pointer assumed to be valid. - unsafe { selinux_sys::context_free(self.0.as_ptr()) } - } - } - - #[derive(Debug)] - pub(crate) struct FileContext { - pub context: *mut c_char, - pub len: usize, - pub allocated: bool, - } - - impl FileContext { - pub(crate) fn new(path: &Path, follow_symbolic_links: bool) -> io::Result { - let c_path = os_str_to_c_string(path.as_os_str())?; - let mut context: *mut c_char = ptr::null_mut(); - - // SAFETY: We assume calling getfilecon()/lgetfilecon() is safe with - // non-null `path` and `context` pointers assumed to be valid. - let len = if follow_symbolic_links { - unsafe { selinux_sys::getfilecon(c_path.as_ptr(), &mut context) } - } else { - unsafe { selinux_sys::lgetfilecon(c_path.as_ptr(), &mut context) } - }; - - if len == -1 { - let err = io::Error::last_os_error(); - if let Some(libc::ENODATA) = err.raw_os_error() { - Ok(Self::default()) - } else { - Err(err) - } - } else if context.is_null() { - Ok(Self::default()) - } else { - Ok(Self { - context, - len: len as usize, - allocated: true, - }) - } - } - - pub(crate) fn from_ptr(context: *mut c_char) -> Self { - if context.is_null() { - Self::default() - } else { - // SAFETY: We assume calling strlen() is safe with a non-null - // `context` pointer assumed to be valid. - let len = unsafe { libc::strlen(context) }; - Self { - context, - len, - allocated: false, - } - } - } - - pub(crate) fn set_for_file( - &mut self, - path: &Path, - follow_symbolic_links: bool, - ) -> io::Result<()> { - let c_path = os_str_to_c_string(path.as_os_str())?; - - // SAFETY: We assume calling setfilecon()/lsetfilecon() is safe with - // non-null `path` and `context` pointers assumed to be valid. - let r = if follow_symbolic_links { - unsafe { selinux_sys::setfilecon(c_path.as_ptr(), self.context) } - } else { - unsafe { selinux_sys::lsetfilecon(c_path.as_ptr(), self.context) } - }; - - if r == -1 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } - } - - pub(crate) fn as_ptr(&self) -> *const c_char { - self.context - } - - pub(crate) fn is_empty(&self) -> bool { - self.context.is_null() || self.len == 0 - } - - pub(crate) fn as_bytes(&self) -> &[u8] { - if self.context.is_null() { - &[] - } else { - // SAFETY: `self.0.context` is a non-null pointer that is assumed to be valid. - unsafe { slice::from_raw_parts(self.context.cast(), self.len) } - } - } - } - - impl Default for FileContext { - fn default() -> Self { - Self { - context: ptr::null_mut(), - len: 0, - allocated: false, - } - } - } - - impl Drop for FileContext { - fn drop(&mut self) { - if self.allocated && !self.context.is_null() { - // SAFETY: We assume calling freecon() is safe with a non-null - // `context` pointer assumed to be valid. - unsafe { selinux_sys::freecon(self.context) } - } + Self::File(context) => context + .to_c_string() + .map_err(|r| Error::from_selinux("SELinuxSecurityContext::to_c_string()", r)), + + Self::String(context) => Ok(context.as_deref().map(Cow::Borrowed)), } } } diff --git a/src/uu/chcon/src/errors.rs b/src/uu/chcon/src/errors.rs new file mode 100644 index 000000000..ecbd7d409 --- /dev/null +++ b/src/uu/chcon/src/errors.rs @@ -0,0 +1,71 @@ +use std::ffi::OsString; +use std::fmt::Write; +use std::io; + +pub(crate) type Result = std::result::Result; + +#[derive(thiserror::Error, Debug)] +pub(crate) enum Error { + #[error("No context is specified")] + MissingContext, + + #[error("No files are specified")] + MissingFiles, + + #[error("{0}")] + ArgumentsMismatch(String), + + #[error(transparent)] + CommandLine(#[from] clap::Error), + + #[error("{operation} failed")] + SELinux { + operation: &'static str, + source: selinux::errors::Error, + }, + + #[error("{operation} failed")] + Io { + operation: &'static str, + source: io::Error, + }, + + #[error("{operation} failed on '{}'", .operand1.to_string_lossy())] + Io1 { + operation: &'static str, + operand1: OsString, + source: io::Error, + }, +} + +impl Error { + pub(crate) fn from_io(operation: &'static str, source: io::Error) -> Self { + Self::Io { operation, source } + } + + pub(crate) fn from_io1( + operation: &'static str, + operand1: impl Into, + source: io::Error, + ) -> Self { + Self::Io1 { + operation, + operand1: operand1.into(), + source, + } + } + + pub(crate) fn from_selinux(operation: &'static str, source: selinux::errors::Error) -> Self { + Self::SELinux { operation, source } + } +} + +pub(crate) fn report_full_error(mut err: &dyn std::error::Error) -> String { + let mut desc = String::with_capacity(256); + write!(&mut desc, "{}", err).unwrap(); + while let Some(source) = err.source() { + err = source; + write!(&mut desc, ". {}", err).unwrap(); + } + desc +} diff --git a/src/uu/chcon/src/fts.rs b/src/uu/chcon/src/fts.rs new file mode 100644 index 000000000..89dd6184d --- /dev/null +++ b/src/uu/chcon/src/fts.rs @@ -0,0 +1,193 @@ +use std::ffi::{CStr, CString, OsStr}; +use std::marker::PhantomData; +use std::os::raw::{c_int, c_long, c_short}; +use std::path::Path; +use std::ptr::NonNull; +use std::{io, iter, ptr, slice}; + +use crate::errors::{Error, Result}; +use crate::os_str_to_c_string; + +#[derive(Debug)] +pub(crate) struct FTS { + fts: ptr::NonNull, + + entry: Option>, + _phantom_data: PhantomData, +} + +impl FTS { + pub(crate) fn new(paths: I, options: c_int) -> Result + where + I: IntoIterator, + I::Item: AsRef, + { + let files_paths: Vec = paths + .into_iter() + .map(|s| os_str_to_c_string(s.as_ref())) + .collect::>()?; + + if files_paths.is_empty() { + return Err(Error::from_io( + "FTS::new()", + io::ErrorKind::InvalidInput.into(), + )); + } + + let path_argv: Vec<_> = files_paths + .iter() + .map(CString::as_ref) + .map(CStr::as_ptr) + .chain(iter::once(ptr::null())) + .collect(); + + // SAFETY: We assume calling fts_open() is safe: + // - `path_argv` is an array holding at least one path, and null-terminated. + // - `compar` is None. + let fts = unsafe { fts_sys::fts_open(path_argv.as_ptr().cast(), options, None) }; + + let fts = ptr::NonNull::new(fts) + .ok_or_else(|| Error::from_io("fts_open()", io::Error::last_os_error()))?; + + Ok(Self { + fts, + entry: None, + _phantom_data: PhantomData, + }) + } + + pub(crate) fn last_entry_ref(&mut self) -> Option { + self.entry.map(move |entry| EntryRef::new(self, entry)) + } + + pub(crate) fn read_next_entry(&mut self) -> Result { + // SAFETY: We assume calling fts_read() is safe with a non-null `fts` + // pointer assumed to be valid. + let new_entry = unsafe { fts_sys::fts_read(self.fts.as_ptr()) }; + + self.entry = NonNull::new(new_entry); + if self.entry.is_none() { + let r = io::Error::last_os_error(); + if let Some(0) = r.raw_os_error() { + Ok(false) + } else { + Err(Error::from_io("fts_read()", r)) + } + } else { + Ok(true) + } + } + + pub(crate) fn set(&mut self, instr: c_int) -> Result<()> { + let fts = self.fts.as_ptr(); + let entry = self + .entry + .ok_or_else(|| Error::from_io("FTS::set()", io::ErrorKind::UnexpectedEof.into()))?; + + // SAFETY: We assume calling fts_set() is safe with non-null `fts` + // and `entry` pointers assumed to be valid. + if unsafe { fts_sys::fts_set(fts, entry.as_ptr(), instr) } == -1 { + Err(Error::from_io("fts_set()", io::Error::last_os_error())) + } else { + Ok(()) + } + } +} + +impl Drop for FTS { + fn drop(&mut self) { + // SAFETY: We assume calling fts_close() is safe with a non-null `fts` + // pointer assumed to be valid. + unsafe { fts_sys::fts_close(self.fts.as_ptr()) }; + } +} + +#[derive(Debug)] +pub(crate) struct EntryRef<'fts> { + pub(crate) pointer: ptr::NonNull, + + _fts: PhantomData<&'fts FTS>, + _phantom_data: PhantomData, +} + +impl<'fts> EntryRef<'fts> { + fn new(_fts: &'fts FTS, entry: ptr::NonNull) -> Self { + Self { + pointer: entry, + _fts: PhantomData, + _phantom_data: PhantomData, + } + } + + fn as_ref(&self) -> &fts_sys::FTSENT { + // SAFETY: `self.pointer` is a non-null pointer that is assumed to be valid. + unsafe { self.pointer.as_ref() } + } + + fn as_mut(&mut self) -> &mut fts_sys::FTSENT { + // SAFETY: `self.pointer` is a non-null pointer that is assumed to be valid. + unsafe { self.pointer.as_mut() } + } + + pub(crate) fn flags(&self) -> c_int { + c_int::from(self.as_ref().fts_info) + } + + pub(crate) fn errno(&self) -> c_int { + self.as_ref().fts_errno + } + + pub(crate) fn level(&self) -> c_short { + self.as_ref().fts_level + } + + pub(crate) fn number(&self) -> c_long { + self.as_ref().fts_number + } + + pub(crate) fn set_number(&mut self, new_number: c_long) { + self.as_mut().fts_number = new_number; + } + + pub(crate) fn path(&self) -> Option<&Path> { + let entry = self.as_ref(); + if entry.fts_pathlen == 0 { + return None; + } + + NonNull::new(entry.fts_path) + .map(|path_ptr| { + let path_size = usize::from(entry.fts_pathlen).saturating_add(1); + + // SAFETY: `entry.fts_path` is a non-null pointer that is assumed to be valid. + unsafe { slice::from_raw_parts(path_ptr.as_ptr().cast(), path_size) } + }) + .and_then(|bytes| CStr::from_bytes_with_nul(bytes).ok()) + .map(c_str_to_os_str) + .map(Path::new) + } + + pub(crate) fn access_path(&self) -> Option<&Path> { + ptr::NonNull::new(self.as_ref().fts_accpath) + .map(|path_ptr| { + // SAFETY: `entry.fts_accpath` is a non-null pointer that is assumed to be valid. + unsafe { CStr::from_ptr(path_ptr.as_ptr()) } + }) + .map(c_str_to_os_str) + .map(Path::new) + } + + pub(crate) fn stat(&self) -> Option<&libc::stat> { + ptr::NonNull::new(self.as_ref().fts_statp).map(|stat_ptr| { + // SAFETY: `entry.fts_statp` is a non-null pointer that is assumed to be valid. + unsafe { stat_ptr.as_ref() } + }) + } +} + +#[cfg(unix)] +fn c_str_to_os_str(s: &CStr) -> &OsStr { + use std::os::unix::ffi::OsStrExt; + + OsStr::from_bytes(s.to_bytes()) +} diff --git a/src/uu/id/Cargo.toml b/src/uu/id/Cargo.toml index 518de2729..b58c7fd78 100644 --- a/src/uu/id/Cargo.toml +++ b/src/uu/id/Cargo.toml @@ -18,7 +18,7 @@ path = "src/id.rs" clap = { version = "2.33", features = ["wrap_help"] } uucore = { version=">=0.0.9", package="uucore", path="../../uucore", features=["entries", "process"] } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } -selinux = { version="0.1.3", optional = true } +selinux = { version="0.2.1", optional = true } [[bin]] name = "id" diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 1dd1b176d..61684c829 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -40,8 +40,6 @@ extern crate uucore; use clap::{crate_version, App, Arg}; -#[cfg(all(target_os = "linux", feature = "selinux"))] -use selinux; use std::ffi::CStr; use uucore::entries::{self, Group, Locate, Passwd}; use uucore::error::UResult;