hur.st's bl.aagh

BSD, Ruby, Rust, Rambling

faccess

Basic cross-platform file accessibility checks for Rust

[rust]

faccess adds file accessbility checks for file paths to Rust:

pub trait PathExt {
    fn access(&self, mode: AccessMode) -> std::io::Result<()>;
    fn readable(&self) -> bool;
    fn writable(&self) -> bool;
    fn executable(&self) -> bool;
}

impl PathExt for std::path::Path;

Usage

For the simple case:

use std::path::PathBuf;
use faccess::PathExt;

let path = PathBuf::from("/path/to/file");
if path.readable() {
  println!("{} was probably readable!", path.display());
}

More complex needs, such as reporting precisely why the file isn’t deemed accessible in the desired manner, the underlying access method can be used:

use std::path::PathBuf;
use faccess::{AccessMode, PathExt};

let path = PathBuf::from("/path/to/file");
match path.access(AccessMode::READ) {
  Ok(()) => println!("{} was probably readable!", path.display()),
  Err(e) => println!("Error: {:?}", e),
};

Technical Details

On Unix platforms, this is basically trivial – just exposing a safe interface to faccessat(2) via the libc crate:

fn eaccess(p: &Path, mode: c_int) -> io::Result<()> {
    let path = CString::new(p.as_os_str().as_bytes())?;
    unsafe {
        if faccessat(AT_FDCWD, path.as_ptr() as *const c_char, mode, AT_EACCESS) == 0 {
            Ok(())
        } else {
            Err(io::Error::last_os_error())
        }
    }
}

On Windows, there’s AccessCheck, which… is both more fiddly and has more special-cases to worry about.

For example, you can’t just ask if the current process can read or write a file or directory, beacuse that would be silly – instead, you want to check if you can access a given SECURITY_DESCRIPTOR that idenfities the resource you wish to check, and check it against an “impersonation token” with TOKEN_QUERY access.

I’m still not entirely happy with what I’ve got – not least because it simply falls back to just trying to open a file in the common case.

This is particularly awkward for me as it interacts badly with Windows Overlay Filter compressed files (as created by Compactor), because checking if such a file is writable will–in opening the file for writing–also uncompress it.

Security Considerations: TOCTOU

This style of file accessibility check is the poster child of so-called “time-of-check to time-of-use” bugs. Here’s Wikipedia’s main example:

if (access("file", W_OK) != 0) {
    exit(1);
}

fd = open("file", O_WRONLY);
write(fd, buffer, sizeof(buffer));

In this C program, file is checked using access in a setuid program to determine if the real user (as opposed to the effective user–setuid programs run as the owner of the executable rather than the actual user running it) is permitted to write to it–basically attempting to replicate the standard file permissions in userspace.

In a multitasking environment this races against other processes on the system, which may manipulate file in between the original access check and the open and write call, causing the program to misuse its elevated permissions to overwrite a file it shouldn’t have had access to.

Happily we can now reproduce such security concerns in Rust, so they can at least be memory-safe. You’re welcome.