Skip to content
Snippets Groups Projects
Commit e46edc21 authored by Nick Mathewson's avatar Nick Mathewson :game_die:
Browse files

Merge branch 'fs-mistrust-v2' into 'main'

Second cut at a fs-mistrust crate.

See merge request tpo/core/arti!468
parents 78ca8731 f35b4881
No related branches found
No related tags found
1 merge request!468Second cut at a fs-mistrust crate.
......@@ -1189,6 +1189,19 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fs-mistrust"
version = "0.1.0"
dependencies = [
"anyhow",
"libc",
"once_cell",
"tempfile",
"thiserror",
"users",
"walkdir",
]
[[package]]
name = "fsevent"
version = "0.4.0"
......@@ -3855,6 +3868,16 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "users"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
dependencies = [
"libc",
"log",
]
[[package]]
name = "valuable"
version = "0.1.0"
......
......@@ -8,6 +8,7 @@
members = [
"crates/tor-basic-utils",
"crates/caret",
"crates/fs-mistrust",
"crates/retry-error",
"crates/tor-error",
"crates/tor-config",
......
[package]
name = "fs-mistrust"
version = "0.1.0"
authors = ["The Tor Project, Inc.", "Nick Mathewson <nickm@torproject.org>"]
edition = "2018"
license = "MIT OR Apache-2.0"
homepage = "https://gitlab.torproject.org/tpo/core/arti/-/wikis/home"
description = "Ensure that files can only be read or written by trusted users"
keywords = ["fs", "file", "permissions", "ownership", "privacy"]
categories = ["filesystem"]
repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
[features]
default = ["walkdir"]
[dependencies]
thiserror = "1"
walkdir = { version = "2", optional = true }
[target.'cfg(unix)'.dependencies]
libc = "0.2"
users = "0.11"
once_cell = "1"
[dev-dependencies]
anyhow = "1.0.23"
tempfile = "3"
This file will be replaced with an automatically generated
document when we next run maint/readmes.
//! Implement a wrapper for access to the members of a directory whose status
//! we've checked.
use std::{
fs::{File, OpenOptions},
path::{Path, PathBuf},
};
use crate::{walk::PathType, Error, Mistrust, Result, Verifier};
#[cfg(target_family = "unix")]
use std::os::unix::fs::OpenOptionsExt;
/// A directory whose access properties we have verified, along with accessor
/// functions to access members of that directory.
///
/// The accessor functions will enforce that whatever security properties we
/// checked on the the directory also apply to all of the members that we access
/// within the directory.
///
/// ## Limitations
///
/// Having a `CheckedDir` means only that, at the time it was created, we were
/// confident that no _untrusted_ user could access it inappropriately. It is
/// still possible, after the `CheckedDir` is created, that a _trusted_ user can
/// alter its permissions, make its path point somewhere else, or so forth.
///
/// If this kind of time-of-use/time-of-check issue is unacceptable, you may
/// wish to look at other solutions, possibly involving `openat()` or related
/// APIs.
///
/// See also the crate-level [Limitations](crate#limitations) section.
pub struct CheckedDir {
/// The `Mistrust` object whose rules we apply to members of this directory.
mistrust: Mistrust,
/// The location of this directory, in its original form.
location: PathBuf,
/// The "readable_okay" flag that we used to create this CheckedDir.
readable_okay: bool,
}
impl CheckedDir {
/// Create a CheckedDir.
pub(crate) fn new(verifier: &Verifier<'_>, path: &Path) -> Result<Self> {
let mut mistrust = verifier.mistrust.clone();
// Ignore the path that we already verified. Since ignore_prefix
// canonicalizes the path, we _will_ recheck the directory if it starts
// pointing to a new canonical location. That's probably a feature.
//
// TODO:
// * If `path` is a prefix of the original ignored path, this will
// make us ignore _less_.
mistrust.ignore_prefix(path)?;
Ok(CheckedDir {
mistrust,
location: path.to_path_buf(),
readable_okay: verifier.readable_okay,
})
}
/// Construct a new directory within this CheckedDir, if it does not already
/// exist.
///
/// `path` must be a relative path to the new directory, containing no `..`
/// components.
pub fn make_directory<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let path = path.as_ref();
self.check_path(path)?;
self.verifier().make_directory(self.location.join(path))
}
/// Open a file within this CheckedDir, using a set of [`OpenOptions`].
///
/// `path` must be a relative path to the new directory, containing no `..`
/// components. We check, but do not create, the file's parent directories.
/// We check the file's permissions after opening it. If the file already
/// exists, it must not be a symlink.
///
/// If the file is created (and this is a unix-like operating system), we
/// always create it with mode `600`, regardless of any mode options set in
/// `options`.
pub fn open<P: AsRef<Path>>(&self, path: P, options: &OpenOptions) -> Result<File> {
let path = path.as_ref();
self.check_path(path)?;
let path = self.location.join(path);
if let Some(parent) = path.parent() {
self.verifier().check(parent)?;
}
#[allow(unused_mut)]
let mut options = options.clone();
#[cfg(target_family = "unix")]
{
// By default, create all files mode 600, no matter what
// OpenOptions said.
// TODO: Give some way to override this to 640 or 0644 if you
// really want to.
options.mode(0o600);
// Don't follow symlinks out of the secured directory.
options.custom_flags(libc::O_NOFOLLOW);
}
let file = options
.open(&path)
.map_err(|e| Error::inspecting(e, &path))?;
let meta = file.metadata().map_err(|e| Error::inspecting(e, &path))?;
if let Some(error) = self
.verifier()
.check_one(path.as_path(), PathType::Content, &meta)
.into_iter()
.next()
{
Err(error)
} else {
Ok(file)
}
}
/// Return a reference to this directory as a [`Path`].
///
/// Note that this function lets you work with a broader collection of
/// functions, including functions that might let you access or create a
/// file that is accessible by non-trusted users. Be careful!
pub fn as_path(&self) -> &Path {
self.location.as_path()
}
/// Helper: create a [`Verifier`] with the appropriate rules for this
/// `CheckedDir`.
fn verifier(&self) -> Verifier<'_> {
let mut v = self.mistrust.verifier();
if self.readable_okay {
v = v.permit_readable();
}
v
}
/// Helper: Make sure that the path `p` is a relative path that can be
/// guaranteed to stay within this directory.
fn check_path(&self, p: &Path) -> Result<()> {
use std::path::Component;
if p.is_absolute() {}
for component in p.components() {
match component {
Component::Prefix(_) | Component::RootDir | Component::ParentDir => {
return Err(Error::InvalidSubdirectory)
}
Component::CurDir | Component::Normal(_) => {}
}
}
Ok(())
}
}
#[cfg(test)]
mod test {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::testing::Dir;
use std::io::Write;
#[test]
fn easy_case() {
let d = Dir::new();
d.dir("a/b/c");
d.dir("a/b/d");
d.file("a/b/c/f1");
d.file("a/b/c/f2");
d.file("a/b/d/f3");
d.chmod("a", 0o755);
d.chmod("a/b", 0o700);
d.chmod("a/b/c", 0o700);
d.chmod("a/b/d", 0o777);
d.chmod("a/b/c/f1", 0o600);
d.chmod("a/b/c/f2", 0o666);
d.chmod("a/b/d/f3", 0o600);
let mut m = Mistrust::new();
m.ignore_prefix(d.canonical_root()).unwrap();
let sd = m.verifier().secure_dir(d.path("a/b")).unwrap();
// Try make_directory.
sd.make_directory("c/sub1").unwrap();
#[cfg(target_family = "unix")]
{
let e = sd.make_directory("d/sub2").unwrap_err();
assert!(matches!(e, Error::BadPermission(_, _)));
}
// Try opening a file that exists.
let f1 = sd.open("c/f1", OpenOptions::new().read(true)).unwrap();
drop(f1);
#[cfg(target_family = "unix")]
{
let e = sd.open("c/f2", OpenOptions::new().read(true)).unwrap_err();
assert!(matches!(e, Error::BadPermission(_, _)));
let e = sd.open("d/f3", OpenOptions::new().read(true)).unwrap_err();
assert!(matches!(e, Error::BadPermission(_, _)));
}
// Try creating a file.
let mut f3 = sd
.open("c/f-new", OpenOptions::new().write(true).create(true))
.unwrap();
f3.write_all(b"Hello world").unwrap();
drop(f3);
#[cfg(target_family = "unix")]
{
let e = sd
.open("d/f-new", OpenOptions::new().write(true).create(true))
.unwrap_err();
assert!(matches!(e, Error::BadPermission(_, _)));
}
}
#[test]
fn bad_paths() {
let d = Dir::new();
d.dir("a");
d.chmod("a", 0o700);
let mut m = Mistrust::new();
m.ignore_prefix(d.canonical_root()).unwrap();
let sd = m.verifier().secure_dir(d.path("a")).unwrap();
let e = sd.make_directory("hello/../world").unwrap_err();
assert!(matches!(e, Error::InvalidSubdirectory));
let e = sd.make_directory("/hello").unwrap_err();
assert!(matches!(e, Error::InvalidSubdirectory));
sd.make_directory("hello/world").unwrap();
}
}
//! Declare an Error type for `fs-mistrust`.
use std::path::Path;
use std::{path::PathBuf, sync::Arc};
use std::io::{Error as IoError, ErrorKind as IoErrorKind};
/// An error returned while checking a path for privacy.
#[derive(Clone, Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// A target (or one of its ancestors) was not found.
#[error("File or directory {0} not found")]
NotFound(PathBuf),
/// A target (or one of its ancestors) had incorrect permissions.
///
/// Only generated on unix-like systems.
///
/// The provided integer contains the `st_mode` bits which were incorrectly
/// set.
#[error("Incorrect permissions on file or directory {0}: {}", format_access_bits(* .1))]
BadPermission(PathBuf, u32),
/// A target (or one of its ancestors) had an untrusted owner.
///
/// Only generated on unix-like systems.
///
/// The provided integer contains the user_id o
#[error("Bad owner (UID {1}) on file or directory {0}")]
BadOwner(PathBuf, u32),
/// A target (or one of its ancestors) had the wrong type.
///
/// Ordinarily, the target may be anything at all, though you can override
/// this with [`require_file`](crate::Verifier::require_file) and
/// [`require_directory`](crate::Verifier::require_directory).
#[error("Wrong type of file at {0}")]
BadType(PathBuf),
/// We were unable to inspect the target or one of its ancestors.
///
/// (Ironically, we might lack permissions to see if something's permissions
/// are correct.)
///
/// (The `std::io::Error` that caused this problem is wrapped in an `Arc` so
/// that our own [`Error`] type can implement `Clone`.)
#[error("Unable to access {0}")]
CouldNotInspect(PathBuf, #[source] Arc<IoError>),
/// Multiple errors occurred while inspecting the target.
///
/// This variant will only be returned if the caller specifically asked for
/// it by calling [`all_errors`](crate::Verifier::all_errors).
///
/// We will never construct an instance of this variant with an empty `Vec`.
#[error("Multiple errors found")]
Multiple(Vec<Box<Error>>),
/// We've realized that we can't finish resolving our path without taking
/// more than the maximum number of steps. The likeliest explanation is a
/// symlink loop.
#[error("Too many steps taken or planned: Possible symlink loop?")]
StepsExceeded,
/// We can't find our current working directory, or we found it but it looks
/// impossible.
#[error("Problem finding current directory")]
CurrentDirectory(#[source] Arc<IoError>),
/// We tried to create a directory, and encountered a failure in doing so.
#[error("Problem creating directory")]
CreatingDir(#[source] Arc<IoError>),
/// We found a problem while checking the contents of the directory.
#[error("Invalid directory content")]
Content(#[source] Box<Error>),
/// We were unable to inspect the contents of the directory
///
/// This error is only present when the `walkdir` feature is enabled.
#[cfg(feature = "walkdir")]
#[error("Unable to list directory")]
Listing(#[source] Arc<walkdir::Error>),
/// We were unable to open a file with [`CheckedDir::open`](crate::CheckedDir::open)
/// Tried to use an invalid path with a [`CheckedDir`](crate::CheckedDir),
#[error("Path was not valid for use with CheckedDir.")]
InvalidSubdirectory,
}
impl Error {
/// Create an error from an IoError object.
pub(crate) fn inspecting(err: IoError, fname: impl Into<PathBuf>) -> Self {
match err.kind() {
IoErrorKind::NotFound => Error::NotFound(fname.into()),
_ => Error::CouldNotInspect(fname.into(), Arc::new(err)),
}
}
/// Return the path, if any, associated with this error.
pub fn path(&self) -> Option<&Path> {
Some(
match self {
Error::NotFound(pb) => pb,
Error::BadPermission(pb, _) => pb,
Error::BadOwner(pb, _) => pb,
Error::BadType(pb) => pb,
Error::CouldNotInspect(pb, _) => pb,
Error::Multiple(_) => return None,
Error::StepsExceeded => return None,
Error::CurrentDirectory(_) => return None,
Error::CreatingDir(_) => return None,
Error::InvalidSubdirectory => return None,
Error::Content(e) => return e.path(),
Error::Listing(e) => return e.path(),
}
.as_path(),
)
}
/// Return an iterator over all of the errors contained in this Error.
///
/// If this is a singleton, the iterator returns only a single element.
/// Otherwise, it returns all the elements inside the `Error::Multiple`
/// variant.
///
/// Does not recurse, since we do not create nested instances of
/// `Error::Multiple`.
pub fn errors<'a>(&'a self) -> impl Iterator<Item = &Error> + 'a {
let result: Box<dyn Iterator<Item = &Error> + 'a> = match self {
Error::Multiple(v) => Box::new(v.iter().map(|e| e.as_ref())),
_ => Box::new(vec![self].into_iter()),
};
result
}
}
impl std::iter::FromIterator<Error> for Option<Error> {
fn from_iter<T: IntoIterator<Item = Error>>(iter: T) -> Self {
let mut iter = iter.into_iter();
let first_err = iter.next()?;
if let Some(second_err) = iter.next() {
let mut errors = Vec::with_capacity(iter.size_hint().0 + 2);
errors.push(Box::new(first_err));
errors.push(Box::new(second_err));
errors.extend(iter.map(Box::new));
Some(Error::Multiple(errors))
} else {
Some(first_err)
}
}
}
/// Convert the low 9 bits of `bits` into a unix-style string describing its
/// access permission.
///
/// For example, 0o022 becomes 'g+w o+w'.
///
/// Used for generating error messages.
fn format_access_bits(bits: u32) -> String {
let mut s = String::new();
for (shift, prefix) in [(6, "u="), (3, "g="), (0, "o=")] {
let b = (bits >> shift) & 7;
if b != 0 {
if !s.is_empty() {
s.push(' ');
}
s.push_str(prefix);
for (bit, ch) in [(4, 'r'), (2, 'w'), (1, 'x')] {
if b & bit != 0 {
s.push(ch);
}
}
}
}
s
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn bits() {
assert_eq!(format_access_bits(0o777), "u=rwx g=rwx o=rwx");
assert_eq!(format_access_bits(0o022), "g=w o=w");
assert_eq!(format_access_bits(0o022), "g=w o=w");
assert_eq!(format_access_bits(0o020), "g=w");
assert_eq!(format_access_bits(0), "");
}
}
//! Implementation logic for `fs-mistrust`.
use std::{
fs::{FileType, Metadata},
path::Path,
};
#[cfg(target_family = "unix")]
use std::os::unix::prelude::MetadataExt;
use crate::{
walk::{PathType, ResolvePath},
Error, Result, Type,
};
/// Definition for the "sticky bit", which on Unix means that the contents of
/// directory may not be renamed, deleted, or otherwise modified by a non-owner
/// of those contents, even if the user has write permissions on the directory.
///
/// This is the usual behavior for /tmp: You can make your own directories in
/// /tmp, but you can't modify other people's.
///
/// (We'd use libc's version of `S_ISVTX`, but they vacillate between u16 and
/// u32 depending what platform you're on.)
#[cfg(target_family = "unix")]
pub(crate) const STICKY_BIT: u32 = 0o1000;
/// Helper: Box an iterator of errors.
fn boxed<'a, I: Iterator<Item = Error> + 'a>(iter: I) -> Box<dyn Iterator<Item = Error> + 'a> {
Box::new(iter)
}
impl<'a> super::Verifier<'a> {
/// Return an iterator of all the security problems with `path`.
///
/// If the iterator is empty, then there is no problem with `path`.
//
// TODO: This iterator is not fully lazy; sometimes, calls to check_one()
// return multiple errors when it would be better for them to return only
// one (since we're ignoring errors after the first). This might be nice
// to fix in the future if we can do so without adding much complexity
// to the code. It's not urgent, since the allocations won't cost much
// compared to the filesystem access.
pub(crate) fn check_errors(&self, path: &Path) -> impl Iterator<Item = Error> + '_ {
if self.mistrust.disable_ownership_and_permission_checks {
// We don't want to walk the path in this case at all: we'll just
// look at the last element.
let meta = match path.metadata() {
Ok(meta) => meta,
Err(e) => return boxed(vec![Error::inspecting(e, path)].into_iter()),
};
let mut errors = Vec::new();
self.check_type(path, PathType::Final, &meta, &mut errors);
return boxed(errors.into_iter());
}
let rp = match ResolvePath::new(path) {
Ok(rp) => rp,
Err(e) => return boxed(vec![e].into_iter()),
};
// Filter to remove every path that is a prefix of ignore_prefix. (IOW,
// if stop_at_dir is /home/arachnidsGrip, real_stop_at_dir will be
// /home, and we'll ignore / and /home.)
let should_retain = move |r: &Result<_>| match (r, &self.mistrust.ignore_prefix) {
(Ok((p, _, _)), Some(ignore_prefix)) => !ignore_prefix.starts_with(p),
(_, _) => true,
};
boxed(
rp.filter(should_retain)
// Finally, check the path for errors.
//
// See `check_one` below for a note on TOCTOU issues.
.flat_map(move |r| match r {
Ok((path, path_type, metadata)) => {
self.check_one(path.as_path(), path_type, &metadata)
}
Err(e) => vec![e],
}),
)
}
/// If check_contents is set, return an iterator over all the errors in
/// elements _contained in this directory_.
#[cfg(feature = "walkdir")]
pub(crate) fn check_content_errors(&self, path: &Path) -> impl Iterator<Item = Error> + '_ {
use std::sync::Arc;
if !self.check_contents || self.mistrust.disable_ownership_and_permission_checks {
return boxed(std::iter::empty());
}
boxed(
walkdir::WalkDir::new(path)
.follow_links(false)
.min_depth(1)
.into_iter()
.flat_map(move |ent| match ent {
Err(err) => vec![Error::Listing(Arc::new(err))],
Ok(ent) => match ent.metadata() {
Ok(meta) => self
.check_one(ent.path(), PathType::Content, &meta)
.into_iter()
.map(|e| Error::Content(Box::new(e)))
.collect(),
Err(err) => vec![Error::Listing(Arc::new(err))],
},
}),
)
}
/// Return an empty iterator.
#[cfg(not(feature = "walkdir"))]
pub(crate) fn check_content_errors(&self, _path: &Path) -> impl Iterator<Item = Error> + '_ {
std::iter::empty()
}
/// Check a single `path` for conformance with this `Verifier`.
///
/// Note that this result is only meaningful if all of the _ancestors_ of
/// this path have been checked. Otherwise, a non-trusted user could change
/// where this path points after it has been checked.
#[must_use]
pub(crate) fn check_one(
&self,
path: &Path,
path_type: PathType,
meta: &Metadata,
) -> Vec<Error> {
let mut errors = Vec::new();
self.check_type(path, path_type, meta, &mut errors);
#[cfg(target_family = "unix")]
self.check_permissions(path, path_type, meta, &mut errors);
errors
}
/// Check whether a given file has the correct type, and push an error into
/// `errors` if not. Other inputs are as for `check_one`.
fn check_type(
&self,
path: &Path,
path_type: PathType,
meta: &Metadata,
errors: &mut Vec<Error>,
) {
let want_type = match path_type {
PathType::Symlink => {
// There's nothing to check on a symlink encountered _while
// looking up the target_; its permissions and ownership do not
// actually matter.
return;
}
PathType::Intermediate => Type::Dir,
PathType::Final => self.enforce_type,
PathType::Content => Type::DirOrFile,
};
if !want_type.matches(meta.file_type()) {
errors.push(Error::BadType(path.into()));
}
}
/// Check whether a given file has the correct ownership and permissions,
/// and push errors into `errors` if not. Other inputs are as for
/// `check_one`.
#[cfg(target_family = "unix")]
fn check_permissions(
&self,
path: &Path,
path_type: PathType,
meta: &Metadata,
errors: &mut Vec<Error>,
) {
// We need to check that the owner is trusted, since the owner can
// always change the permissions of the object. (If we're talking
// about a directory, the owner cah change the permissions and owner
// of anything in the directory.)
let uid = meta.uid();
if uid != 0 && Some(uid) != self.mistrust.trust_uid {
errors.push(Error::BadOwner(path.into(), uid));
}
let mut forbidden_bits = if !self.readable_okay
&& (path_type == PathType::Final || path_type == PathType::Content)
{
// If this is the target or a content object, and it must not be
// readable, then we forbid it to be group-rwx and all-rwx.
0o077
} else {
// If this is the target object and it may be readable, or if
// this is _any parent directory_, then we typically forbid the
// group-write and all-write bits. (Those are the bits that
// would allow non-trusted users to change the object, or change
// things around in a directory.)
if meta.is_dir() && meta.mode() & STICKY_BIT != 0 && path_type == PathType::Intermediate
{
// This is an intermediate directory and this sticky bit is
// set. Thus, we don't care if it is world-writable or
// group-writable, since only the _owner_ of a file in this
// directory can move or rename it.
0o000
} else {
// It's not a sticky-bit intermediate directory; actually
// forbid 022.
0o022
}
};
// If we trust the GID, then we allow even more bits to be set.
if self.mistrust.trust_gid == Some(meta.gid()) {
forbidden_bits &= !0o070;
}
let bad_bits = meta.mode() & forbidden_bits;
if bad_bits != 0 {
errors.push(Error::BadPermission(path.into(), bad_bits));
}
}
}
impl super::Type {
/// Return true if this required type is matched by a given `FileType`
/// object.
fn matches(&self, have_type: FileType) -> bool {
match self {
Type::Dir => have_type.is_dir(),
Type::File => have_type.is_file(),
Type::DirOrFile => have_type.is_dir() || have_type.is_file(),
Type::Anything => true,
}
}
}
This diff is collapsed.
//! Testing support functions, to more easily make a bunch of directories and
//! links.
//!
//! This module is only built when compiling tests.
use std::{
fs::{self, File},
io::Write,
path::{Path, PathBuf},
};
#[cfg(target_family = "unix")]
use std::os::unix::{self, fs::PermissionsExt};
#[cfg(target_family = "windows")]
use std::os::windows;
/// A temporary directory with convenience functions to build items inside it.
#[derive(Debug)]
pub(crate) struct Dir {
/// The temporary directory
toplevel: tempfile::TempDir,
/// Canonicalized path to the temporary directory
canonical_root: PathBuf,
}
/// When creating a link, are we creating a directory link or a file link?
///
/// (These are the same on Unix, and different on windows.)
#[derive(Copy, Clone, Debug)]
pub(crate) enum LinkType {
Dir,
File,
}
impl Dir {
/// Make a new temporary directory
pub(crate) fn new() -> Self {
let toplevel = tempfile::TempDir::new().expect("Can't get tempfile");
let canonical_root = toplevel.path().canonicalize().expect("Can't canonicalize");
Dir {
toplevel,
canonical_root,
}
}
/// Return the canonical path of the directory's root.
pub(crate) fn canonical_root(&self) -> &Path {
self.canonical_root.as_path()
}
/// Return the path to the temporary directory's root relative to our working directory.
pub(crate) fn relative_root(&self) -> PathBuf {
let mut cwd = std::env::current_dir().expect("no cwd");
let mut relative = PathBuf::new();
// TODO(nickm): I am reasonably confident that this will not work
// correctly on windows.
while !self.toplevel.path().starts_with(&cwd) {
assert!(cwd.pop());
relative.push("..");
}
relative.join(
self.toplevel
.path()
.strip_prefix(cwd)
.expect("error computing common ancestor"),
)
}
/// Return the path of `p` within this temporary directory.
///
/// Requires that `p` is a relative path.
pub(crate) fn path(&self, p: impl AsRef<Path>) -> PathBuf {
let p = p.as_ref();
assert!(p.is_relative());
self.canonical_root.join(p)
}
/// Make a directory at `p` within this temporary directory, creating
/// parent directories as needed.
///
/// Requires that `p` is a relative path.
pub(crate) fn dir(&self, p: impl AsRef<Path>) {
fs::create_dir_all(self.path(p)).expect("Can't create directory.");
}
/// Make a small file at `p` within this temporary directory, creating
/// parent directories as needed.
///
/// Requires that `p` is a relative path.
pub(crate) fn file(&self, p: impl AsRef<Path>) {
self.dir(p.as_ref().parent().expect("Tempdir had no parent"));
let mut f = File::create(self.path(p)).expect("Can't create file");
f.write_all(&b"This space is intentionally left blank"[..])
.expect("Can't write");
}
/// Make a relative link from "original" to "link" within this temporary
/// directory, where `original` is relative
/// to the directory containing `link`, and `link` is relative to the temporary directory.
pub(crate) fn link_rel(
&self,
link_type: LinkType,
original: impl AsRef<Path>,
link: impl AsRef<Path>,
) {
#[cfg(target_family = "unix")]
{
let _ = link_type;
unix::fs::symlink(original.as_ref(), self.path(link)).expect("Can't symlink");
}
#[cfg(target_family = "windows")]
match link_type {
LinkType::Dir => windows::fs::symlink_dir(original.as_ref(), self.path(link)),
LinkType::File => windows::fs::symlink_file(original.as_ref(), self.path(link)),
}
.expect("Can't symlink");
}
/// As `link_rel`, but create an absolute link. `original` is now relative
/// to the temporary directory.
pub(crate) fn link_abs(
&self,
link_type: LinkType,
original: impl AsRef<Path>,
link: impl AsRef<Path>,
) {
self.link_rel(link_type, self.path(original), link);
}
/// Change the unix permissions of a file.
///
/// Requires that `p` is a relative path.
///
/// Does nothing on windows.
pub(crate) fn chmod(&self, p: impl AsRef<Path>, mode: u32) {
#[cfg(target_family = "unix")]
{
let perm = fs::Permissions::from_mode(mode);
fs::set_permissions(self.path(p), perm).expect("can't chmod");
}
#[cfg(not(target_family = "unix"))]
{
let (_, _) = (p, mode);
}
}
}
//! Code to inspect user db information on unix.
use once_cell::sync::Lazy;
use std::{ffi::OsString, sync::Mutex};
/// Cached values of user db entries we've looked up.
///
/// Cacheing here saves time, AND makes our code testable.
///
/// Though this type has interior mutability, it isn't Sync, so we need to add a mutex.
static CACHE: Lazy<Mutex<users::UsersCache>> = Lazy::new(|| Mutex::new(users::UsersCache::new()));
/// Look for a group with the same name as our username.
///
/// If there is one, and we belong to it, return its gid. Otherwise
/// return None.
pub(crate) fn get_self_named_gid() -> Option<u32> {
let cache = CACHE.lock().expect("Poisoned lock");
get_self_named_gid_impl(&*cache)
}
/// Like get_self_named_gid(), but use a provided user database.
fn get_self_named_gid_impl<U: users::Groups + users::Users>(userdb: &U) -> Option<u32> {
let username = get_own_username(userdb)?;
let group = userdb.get_group_by_name(username.as_os_str())?;
// TODO: Perhaps we should enforce a requirement that the group contains
// _only_ the current users. That's kinda tricky to do, though, without
// walking the entire user db.
if cur_groups().contains(&group.gid()) {
Some(group.gid())
} else {
None
}
}
/// Find our username, if possible.
///
/// By default, we look for the USER environment variable, and see whether we an
/// find a user db entry for that username with a UID that matches our own.
///
/// Failing that, we look for a user entry for our current UID.
fn get_own_username<U: users::Users>(userdb: &U) -> Option<OsString> {
let my_uid = userdb.get_current_uid();
if let Some(username) = std::env::var_os("USER") {
if let Some(passwd) = userdb.get_user_by_name(username.as_os_str()) {
if passwd.uid() == my_uid {
return Some(username);
}
}
}
if let Some(passwd) = userdb.get_user_by_uid(my_uid) {
// This check should always pass, but let's be extra careful.
if passwd.uid() == my_uid {
return Some(passwd.name().to_owned());
}
}
None
}
/// Return a vector of the group ID values for every group to which we belong.
///
/// (We don't use `users::group_access_list()` here, since that function calls
/// `getgrnam_r` on every group we belong to, when in fact we don't care what
/// the groups are named.)
fn cur_groups() -> Vec<u32> {
let n_groups = unsafe { libc::getgroups(0, std::ptr::null_mut()) };
if n_groups <= 0 {
return Vec::new();
}
let mut buf: Vec<users::gid_t> = vec![0; n_groups as usize];
let n_groups2 = unsafe { libc::getgroups(buf.len() as i32, buf.as_mut_ptr()) };
if n_groups2 <= 0 {
return Vec::new();
}
if n_groups2 < n_groups {
buf.resize(n_groups2 as usize, 0);
}
buf
}
#[cfg(test)]
mod test {
#![allow(clippy::unwrap_used)]
use super::*;
use users::mock::{Group, MockUsers, User};
#[test]
fn groups() {
let groups = cur_groups();
let cur_gid = users::get_current_gid();
if groups.is_empty() {
// Some container/VM setups forget to put the (root) user into any
// groups at all.
return;
}
assert!(groups.contains(&cur_gid));
}
#[test]
fn username_real() {
// Here we'll do tests with our real username. THere's not much we can
// actually test there, but we'll try anyway.
let cache = CACHE.lock().expect("poisoned lock");
let uname = get_own_username(&*cache).expect("Running on a misconfigured host");
let user = users::get_user_by_name(uname.as_os_str()).unwrap();
assert_eq!(user.name(), uname);
assert_eq!(user.uid(), users::get_current_uid());
}
#[test]
fn username_from_env() {
let username = if let Some(username) = std::env::var_os("USER") {
username
} else {
// Can't test this without setting the environment, and we don't do that in tests.
return;
};
let username_s = if let Some(u) = username.to_str() {
u
} else {
// Can't mock usernames that aren't utf8.
return;
};
let other_name = format!("{}2", username_s);
// Case 1: Current user in environment exists, though there are some distractions.
let mut db = MockUsers::with_current_uid(413);
db.add_user(User::new(413, username_s, 413));
db.add_user(User::new(999, &other_name, 999));
// I'd like to add another user with the same UID and a different name,
// but MockUsers doesn't support that.
let found = get_own_username(&db);
assert_eq!(found.as_ref(), Some(&username));
// Case 2: Current user in environment exists, but has the wrong uid.
let mut db = MockUsers::with_current_uid(413);
db.add_user(User::new(999, username_s, 999));
db.add_user(User::new(413, &other_name, 413));
let found = get_own_username(&db);
assert_eq!(found, Some(OsString::from(other_name.clone())));
// Case 3: Current user in environment does not exist; no user can be found.
let mut db = MockUsers::with_current_uid(413);
db.add_user(User::new(999413, &other_name, 999));
let found = get_own_username(&db);
assert!(found.is_none());
}
#[test]
fn username_ignoring_env() {
// Case 1: uid is found.
let mut db = MockUsers::with_current_uid(413);
db.add_user(User::new(413, "aranea", 413413));
db.add_user(User::new(415, "notyouru!sername", 413413));
let found = get_own_username(&db);
assert_eq!(found, Some(OsString::from("aranea")));
// Case 2: uid not found.
let mut db = MockUsers::with_current_uid(413);
db.add_user(User::new(999413, "notyourn!ame", 999));
let found = get_own_username(&db);
assert!(found.is_none());
}
#[test]
fn selfnamed() {
// check the real groups we're in, since this isn't mockable.
let cur_groups = cur_groups();
if cur_groups.is_empty() {
// Can't actually proceed with the test unless we're in a group.
return;
}
let not_our_gid = (1..65536)
.find(|n| !cur_groups.contains(n))
.expect("We are somehow in all groups 1..65535!");
// Case 1: we find our username but no group with the same name.
let mut db = MockUsers::with_current_uid(413);
db.add_user(User::new(413, "aranea", 413413));
db.add_group(Group::new(413413, "serket"));
let found = get_self_named_gid_impl(&db);
assert!(found.is_none());
// Case 2: we find our username and a group with the same name, but we
// are not a member of that group.
let mut db = MockUsers::with_current_uid(413);
db.add_user(User::new(413, "aranea", 413413));
db.add_group(Group::new(not_our_gid, "aranea"));
let found = get_self_named_gid_impl(&db);
assert!(found.is_none());
// Case 3: we find our username and a group with the same name, AND we
// are indeed a member of that group.
let mut db = MockUsers::with_current_uid(413);
db.add_user(User::new(413, "aranea", 413413));
db.add_group(Group::new(cur_groups[0], "aranea"));
let found = get_self_named_gid_impl(&db);
assert_eq!(found, Some(cur_groups[0]));
}
}
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment