Skip to content

Problems with source tampering resistance #86

@khanhtranngoccva

Description

@khanhtranngoccva

I'm trying to develop the ability of easy_fuser to resist upstream file or directory name tampering. This would happen if the source directory is exposed to the user, or is mounted somewhere else as a bind mount, usually from a user's attempts to rename an existing directory or file inode.

So far, it is slightly easier for the FUSE filesystem to deal with a tampered file if TTL=0 (which is what I use in my application to ensure attributes are not cached and will appear differently per process) and I am opening the file by path, because there is always a lookup call before anything else happens.

However, when TTL != 0 or when opening a persistent directory handle using a path like /fs/path/to/A (which resolves to FUSE inode 3, for example), and the directory or file is renamed to something like /fs/path/to/B, then any further opens related to inode 3 or descendants of inode 3 (including lookup on inode 3) will fail because /fs/path/to/B is not recognized by the mapper yet.

I just wrote a test case on my end-user application to simulate this scenario, and the test, which fails with the current implementation of InodeMultiMapper is as follows:

#[test_log::test(rstest)]
fn test_should_open_inner_file_by_directory_ref_when_directory_name_is_tampered(
    #[from(fixtures::globals)] _globals: &(),
) -> Result<(), anyhow::Error> {
    let test_dir = fixtures::generate_test_dir();
    let mounted_file_system = fixtures::file_system(test_dir.path());
    let target_directory_path = mounted_file_system.target_dir.join("path/to/test_dir");
    fs::create_dir_all(&target_directory_path).expect("should create directory");

    // Stable reference to the target directory
    let target_dir = Dir::open(&target_directory_path).expect("should open target directory");
    let mut inner_file = target_dir
        .write_file("test.txt", 0o644)
        .expect("should create file in target directory");
    inner_file
        .write_all(b"Hello, world!")
        .expect("should write to test file");
    drop(inner_file);

    // Rename the target directory on the source filesystem
    let target_directory_source_path = mounted_file_system.source_dir.join("path/to/test_dir");
    let target_directory_new_source_path = mounted_file_system.source_dir.join("path/to/test_dir_new");
    std::fs::rename(
        &target_directory_source_path,
        &target_directory_new_source_path,
    )
    .expect("should rename directory");

    // The FUSE implementation should try to open file using the `target_dir` handle as a fallback
    let mut inner_file = target_dir
        .open_file("test.txt")
        .expect("should open test file using the directory handle");
    let mut buffer = Vec::new();
    inner_file
        .read_to_end(&mut buffer)
        .expect("should read from test file");
    assert_eq!(buffer, b"Hello, world!");
    drop(inner_file);
    Ok(())
}

My idea is to let the user define a custom cloneable BackingHandle structure (usually an Arc of an underlying type) or another cloneable data structure that they can return alongside the backing ID and the metadata. This structure is then stored in the mapper, and can be retrieved from a method like backing_handle() in the HybridId API.
The HybridId would take another generic parameter so that it is now HybridId<BackingId, BackingHandle>, and backing_handle() shall return Option<BackingHandle>. The concern is that a lot of mapper internals may have to be altered in ways that I have not figured out yet, for example, the FileIdType trait has to add another trait like type BackingHandle = Option<BackingHandle>.

Depending on application, the user can try to exhaust the available paths that the mapper supplies first, then fall back to atomic operations with BackingHandle. Alternatively, the user may exclusively rely on BackingHandle and ignore the path-based APIs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions