Started: `UntrustedPathRules` path-check function pointer table. Currently incomplete, and does not handle multiple masks (i.e X | Y -> X || Y).

Fortune for rng's current commit: Future blessing − 末吉
plugin
Avril 2 years ago
parent b566caa361
commit 430bc78072
Signed by: flanchan
GPG Key ID: 284488987C31F630

@ -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]

@ -0,0 +1 @@
//! The plugin API.

@ -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;

@ -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]
@ -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,10 +418,17 @@ 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())
{
@ -262,31 +436,25 @@ impl PathLookup
_ => 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<impl FnOnce> 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<F: FnOnce(&Path) -> bool>(F);
// impl<F: FnOnce(&Path) -> bool> Checker for FuncChecker<F>
// {
// #[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)
}))
};

Loading…
Cancel
Save