Crate sneak

Crate sneak 

Source
Expand description

High-level abstractions of *at(2)-and-related Linux syscalls to build race condition-free, thread-safe, symlink traversal attack-safe user APIs.

use sneak::{default_flags, openat2, Dir, OpenHow};
use libc::{RESOLVE_BENEATH, O_CREAT, O_WRONLY, O_RDONLY};
use std::io::Write;

let root = Dir::open(".")?;

// Open subdirectories with `openat2` adapters
let appdata = root.open_dirs_beneath(format!("application/data/{user_path}"))?;

// Open successive directories with chained `openat` calls
let sibling = root.open_dirs("../neighbor")?;

// Open files
let mut data = sibling.open_file("data.bin", O_CREAT | O_WRONLY, 0o655)?;
data.write_all(b"hello world!\n");

// Directly use openat2 
let mut how = OpenHow::zeroed();
how.flags = O_RDONLY | O_CREAT;
how.mode = 0o777;
how.resolve = RESOLVE_BENEATH;

let dirfd = openat2(dirfd, "subfolder", &how)?;

§Motivation

While building filesystem-abstracting APIs, you can easily run into race conditions: classic system calls, as exposed by Rust’s filesystem library, often do not provide sufficient protections in multi-threaded or multi-process applications.

In more complex applications, especially if they run as root, you risk exposing yourself to time-of-check time-of-use (TOCTOU) race conditions, which can culminate to privilege escalation vulnerabilities. Up until recently, std::fs::remove_dir_all was sensitive to this attack vector.

Unfortunately, avoiding these race conditions is not an easy task. You need to directly interact with specialized system calls, handle different operating systems and unsafe code. This library aims to provide a safe, easy to use yet ultra flexible API which doesn’t hide away any implementation details.

§Do I need to use sneak?

If your application accesses and modifies a filesystem tree at the same time as another thread or another process, especially if one of these processes runs as root, you should use sneak or any similar library.

use sneak::Dir;

let base_dir = Dir::open(BASE_DIR)?;

println!("uid({})", base_dir.fstat()?.uid());

You can use it within your application to secure your filesystem interactions:

use sneak::Dir;

#[post("/files/upload")]
fn upload(request: &Request, data: Vec<u8>) -> anyhow::Result<()> {
    let user_dir: PathBuf = directory_of_user(request.user_id);
    let user_dir = Dir::open(&user_dir)?;

    // if another application has access to these files at the same time
    // as our API, we can avoid race conditions with sneak:
    let mut data_file = user_dir.open_file(format!("user_data/{}/data.bin", request.user_id), libc::WRONLY)?;

    // set correct file permissions
    data_file.fchown(request.user_uid, request.user_gid)?;

    // write the data
    data_file.write_all(&data)?;

    Ok(())
}

§Async support

A crate like this cannot support async without being runtime-specific. Though, using it as part of your async codebase should be easy: just wrap the syscall-calling operations in your runtime’s spawn_blocking function. This includes all methods on Dir, as well as its Drop implementation.

use std::path::PathBuf;
use std::io;

use sneak::Dir;
use tokio::task::spawn_blocking;
use tokio::fs::File;

/// Example with Tokio.
async fn open_file_async(base_dir: PathBuf, filepath: PathBuf) -> io::Result<File> {
    spawn_blocking(move || {
        let file = Dir::open(&base_dir)?.open_file(&filepath)?;
        Ok(File::from_std(file))
    }).await.expect("I/O task not to panic")
}

§OS Support

This crate exclusively supports Linux. Some methods use the openat2 syscall, which is only supported by Linux 5.6+. You may check for openat2 compatibility with [openat2_compatible].

§Prior art

The openat crate is more widely used and exposes a few more methods, but lacks some flexibility I personally needed.

§License

This software is dual-licensed under the MIT license and the Apache-2.0 license.

Structs§

Dir
A owned reference to an opened directory. This reference is automatically cleaned up on drop.
DirStream
Dirent
Metadata
File or directory metadata. This is analogous to the standard library’s Metadata.
OpenHow
Arguments the behavior of the openat2 syscall.

Functions§

default_flags
Returns the default flags used by Dir. In the majority cases, these flags should be used.
openat2
Wrapper around the openat2 syscall.