diff --git a/Cargo.toml b/Cargo.toml index 977a37d..80eba77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ bitflags = { version = "1.3.2", optional = true } getrandom = "0.1" lazy_static = { version = "1.4.0", optional = true } libc = { version = "0.2.126", optional = true } +readable-perms = "0.1.3" regex = { version = "1.6.0", optional = true } [features] diff --git a/src/plugin/api.rs b/src/plugin/api.rs new file mode 100644 index 0000000..6ab27a2 --- /dev/null +++ b/src/plugin/api.rs @@ -0,0 +1 @@ +//! The plugin API. diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index 2ee70b1..7601bc5 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -1,4 +1,7 @@ //! Allow loading plugins at runtime. +//TODO: Move this to a library crate and re-export it as `plugin::api` +pub mod api; + mod loader; mod searcher; diff --git a/src/plugin/searcher.rs b/src/plugin/searcher.rs index bea6689..cfcb19b 100644 --- a/src/plugin/searcher.rs +++ b/src/plugin/searcher.rs @@ -28,9 +28,12 @@ pub mod locations { bitflags! { /// The rules a file in an unstructed lookup path must qualify for it to be considered for plugin candidacy. - pub struct UntrustedPathRules : u32 + pub struct UntrustedPathRules : u8 { /// No restrictions on the file. + /// + /// # Note + /// Must always be 0. const UNRESTRICTED = 0; /// Name ends in valid string from `LOOKUP_NAME_MATCHES_IN_UNTRUSTED`. const NAME_MATCH = 1 << 0; @@ -46,8 +49,154 @@ pub mod locations { const USER_OWNED_ONLY = 1 << 5; /// Disqualify files that are accessible at all by anyone other than the current user (or root, which can accesss everything.) const USER_ACCESSIBLE_ONLY = 1 << 6; + /// Disallow *all* untrusted files. + /// + /// # Note + /// Must be highest value. + const INACCESSIBLE = 1 << 7; } } + + impl UntrustedPathRules + { + /// Contains function pointers for checking paths with every combination (**any**, not **all** in the mask) of rules in `UntrustedPathRules`. + /// + /// Invalid functions will either: + /// * Panic if they are *never* supposed to be called in the pipeline (e.g `NAME_MATCH`.) + /// * Return false immediately. + pub const RULE_FUNCTION_CHECK_TABLE: [&'static (dyn Fn (&Path, &str) -> bool + 'static); 256] = Self::generate_rule_table(); + + /// Generate a table of 256 entries that which each single, or **OR**-combined mask entry of `UntrustedPathRules` has a corresponding path check function. + /// + /// Invalid functions will either: + /// * Panic if they are *never* supposed to be called in the pipeline (e.g `NAME_MATCH`.) + /// * Return false immediately. + pub(super) const fn generate_rule_table() -> [&'static (dyn Fn (&Path, &str) -> bool + 'static); 256] + { + let mut rule_table = Self::generate_pruned_rule_table(); + //TODO: Add in combinations via ORing its single-bit entry functions. + rule_table + } + /// Generate a table of 256 entries that which each **single** (not combinations of) entry of `UntrustedPathRules` has a corresponding path check function. + #[inline] + const fn generate_pruned_rule_table() -> [&'static (dyn Fn (&Path, &str) -> bool + 'static); 256] + { + type RuleCheckFunc<'a> = (dyn Fn(&Path, &str) -> bool + 'a); + #[inline] + const fn disallow(_: &Path, _: &str) -> bool { false } + + let mut table: [&'static RuleCheckFunc; 256] = [&(disallow as fn(&Path, &str) -> bool); 256]; + + macro_rules! set { + ($func:expr) => { + { + let func: &'static RuleCheckFunc<'static> = &($func); + func + } + }; + ($name:ident => $func:expr) => { + + let bits = Self::$name.bits as usize; + + // Fill in compositions based on previous entries into `table`. + let func: &'static RuleCheckFunc<'static> = set!($func); + /*let rv = { + let mut i: usize = 1; + //XXX: WHY doesn't this work??? + while i < bits { + let prev = table[i]; + + table[bits | i] = set!(|path: &Path, name: &str| -> bool { + prev(path, name) && func(path, name) + }); + + i += 1; + }*/ + table[bits] = func + //}; + //rv + }; + ($name:ident as ! $panic_msg:literal) => { + #[inline(never)] + #[cold] + fn _panic_with_msg() -> ! { + panic!($panic_msg) + } + set!($name => |_, _| _panic_with_msg()) + }; + ($name:ident: $($func_body:tt)+) => { + set!($name => |path, filename| { $($func_body)+}) + } + } + + set!(NAME_MATCH as ! "This should already have been checked"); + set!(NON_SYMLINK => |path: &Path, _| !path.is_symlink()); + set!(NON_EXEC => |path, _| { + use std::os::unix::fs::PermissionsExt as _; + use readable_perms::{ + User, + Bit, + }; + const EXEC_BIT: u32 = + User::Group.mode(Bit::Execute) + | User::Other.mode(Bit::Execute) + | User::Owner.mode(Bit::Execute); + path.metadata() + .map(|meta| meta.permissions().mode() & EXEC_BIT == 0) //TODO: XXX: Test this. + .unwrap_or(false) + }); + set!(NON_WORLD_WRITABLE => |path, _| { + use readable_perms::{ + PermissionsExt as _, + User, + Bit, + }; + path.metadata() + .map(|meta| !meta.permissions().unix().has_mask(User::Other, Bit::Write)) + .unwrap_or(false) + }); + set!(ROOT_OWNED_ONLY => |path, _| { + use std::os::unix::fs::MetadataExt as _; + path.metadata() + .map(|meta| meta.uid() == 0) + .unwrap_or(false) + }); + set!(USER_OWNED_ONLY => |path, _| { + use std::os::unix::fs::MetadataExt as _; + extern "C" { + fn getuid() -> u32; // uid_t + //XXX: Should we use process's UID or EUID? It seems we should use EUID. + fn geteuid() -> u32; // uid_t + } + // SAFETY: This is a pure function for all that matters. + let user_id = unsafe { geteuid() }; + path.metadata() + .map(move |meta| meta.uid() == user_id) + .unwrap_or(false) + }); + set!(USER_ACCESSIBLE_ONLY => |path, _| { + //Only Owner should be able to read, write, or execute. + use std::os::unix::fs::PermissionsExt as _; + use readable_perms::{ + User, + Bit, + }; + const BIT_ANY: Bit = + Bit::Read + .union(Bit::Write) + .union(Bit::Execute); + const OTHER_ACCESSORS: u32 = + User::Group.mode(BIT_ANY) + | User::Other.mode(BIT_ANY); + path.metadata() + .map(|meta| !meta.permissions().mode() & OTHER_ACCESSORS == 0) //XXX: Test this + .unwrap_or(false) + }); + //TODO: Composition of functions should be done via OR, how to insert all possible compositions into table? We get confusing error messages when we try inside `set!()` + table + } + } + impl Default for UntrustedPathRules { #[inline] @@ -103,10 +252,10 @@ pub mod locations { /// Should symlinks be followed in a recursive lookup. (0 is for trusted paths, 1 is for untrusted) pub static LOOKUP_PATH_FOLLOW_SYMLINKS: (bool, bool) = (true, false); - + /// Trust plugin file lookups in non-contained `LOOKUP_PATH_HOME_BASE_NAME`s. pub static LOOKUP_PATH_TRUST_HOME: bool = false; - + /// Base paths from user home to look-up plugin name. pub static LOOKUP_PATH_HOME_BASE_NAME: &[PathTrust] = &[ Trusted(".rngcli/plugins/"), @@ -114,7 +263,7 @@ pub mod locations { Untrusted(".config/rngcli/").map_trust(LOOKUP_PATH_TRUST_HOME), Untrusted(".config/").map_trust(LOOKUP_PATH_TRUST_HOME), ]; - + /// Extra valid lookup paths pub static LOOKUP_PATH_EXTRA_PATHS: &[PathTrust] = &[ Trusted("/usr/share/rngcli/"), @@ -142,7 +291,7 @@ pub mod locations { r"\.rngp\.so$", r"\.rngp$", ]; - + /// These rules must be met for lookups in untrusted paths. pub static LOOKUP_UNTRUSTED_PATH_RULES: UntrustedPathRules = UntrustedPathRules::NAME_MATCH @@ -238,7 +387,25 @@ impl PathLookup fn validate_file_untrusted(path: &Path, filename: &str) -> bool { // By this point, `filename` has already matched successfully. - // Also Note: `filename` is just the basename of `path` as an `&str` (same address-space.) + // Also Note: `filename` is just the basename of `path` as an `&str` (aliasing address-space.) + use locations::UntrustedPathRules as Rule; + use locations::LOOKUP_UNTRUSTED_PATH_RULES as RULES; + + let mut bit = Rule::INACCESSIBLE.bits(); + + // No rules + if bit == 0 /* `Rule::UNRESTRICTED` */ { + return true; + } + + while bit > 0 + { + // SAFETY: We know `INACCESSIBLE` is the largest + if RULES.contains(unsafe { Rule::from_bits_unchecked(bit) }) { + + } + bit >>= 1; + } todo!("TODO: extra Untrusted lookup rules checked on `path` here.") } @@ -251,42 +418,43 @@ impl PathLookup let path = path.as_ref(); + // Check if the path: + // * Exists + // * Is not a directory + // NOTE: We don't use `.is_file()` here, because we want to support symlinks and pseudofiles in trusted contexts, which `is_file()` reports as false. + + // TODO: XXX: Maybe in the future allow directories to be plugins; they would contain a manifest, and each `.so` file in the directory will be loaded in order specified by the directory's manifest. if !path.exists() || path.is_dir() { return false; } + // Attempt to extract the filename, return false if it is invalid UTF8. let filename = match path.file_name() .and_then(|os_fn| os_fn.to_str()) { Some(f) => f, _ => return false, }; - - // trait Checker // XXX: Was used for dynamic dispatch in the `if Trusted ...` block below, but is no longer needed as Trusted will never have one, and untrusted will always have the same one; we can just use Option pretty much - // { - // fn check_path(self, path: &Path) -> bool; - // } - // /*struct NoChecker; - // impl Checker for NoChecker { #[inline] fn check_path(&self, path: &Path) -> bool { - // true - // } }*/ - - // #[derive(Debug, Clone, Copy)] - // struct FuncChecker bool>(F); - // impl bool> Checker for FuncChecker - // { - // #[inline] - // fn check_path(self, path: &Path) -> bool { - // self.0(path) - // } - // } + + // Get the name matcher and rule checker for the file. let (matcher, checker) = if TRUSTED { // Trusted path lookup (match_any(TRUSTED_EXT_REGEX_MAP.iter()), None)//&NoChecker) } else { // Untrusted path lookup - (match_any(UNTRUSTED_EXT_REGEX_MAP.iter()), Some(move |path| { + + let matcher = if locations::LOOKUP_UNTRUSTED_PATH_RULES + .contains(locations::UntrustedPathRules::NAME_MATCH) + { + // Name must match + match_any(UNTRUSTED_EXT_REGEX_MAP.iter()) + } else { + // Name needn't match + match_any(TRUSTED_EXT_REGEX_MAP.iter()) + }; + + (matcher, Some(move |path| { validate_file_untrusted(path, filename) })) };