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.
I'm trying to develop the ability of
easy_fuserto 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/Bis 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:
My idea is to let the user define a custom cloneable
BackingHandlestructure (usually anArcof 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 likebacking_handle()in theHybridIdAPI.The
HybridIdwould take another generic parameter so that it is nowHybridId<BackingId, BackingHandle>, andbacking_handle()shall returnOption<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, theFileIdTypetrait has to add another trait liketype 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 onBackingHandleand ignore the path-based APIs.