use std::mem::size_of;
use std::sync::Arc;
use tempfile::TempDir;
use crate::fuse::*;
use crate::passthrough::PassthroughFs;
use super::*;
fn setup_dispatcher() -> (TempDir, FuseDispatcher) {
let temp = TempDir::new().expect("failed to create temp dir");
let fs = Arc::new(PassthroughFs::new(temp.path()).expect("failed to create fs"));
let dispatcher = FuseDispatcher::new(fs, DispatcherConfig::default());
(temp, dispatcher)
}
fn make_header(opcode: FuseOpcode, nodeid: u64, body_len: usize) -> Vec<u8> {
let header = FuseInHeader {
len: (FuseInHeader::SIZE + body_len) as u32,
opcode: opcode as u32,
unique: 1,
nodeid,
uid: 0,
gid: 0,
pid: 0,
padding: 0,
};
let header_bytes = unsafe {
std::slice::from_raw_parts(
&header as *const FuseInHeader as *const u8,
FuseInHeader::SIZE,
)
};
header_bytes.to_vec()
}
fn parse_response_header(response: &[u8]) -> FuseOutHeader {
assert!(response.len() >= FuseOutHeader::SIZE);
unsafe { std::ptr::read_unaligned(response.as_ptr() as *const FuseOutHeader) }
}
#[test]
fn test_getattr_root() {
let (_temp, dispatcher) = setup_dispatcher();
let request = make_header(FuseOpcode::Getattr, 1, 0);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, 0);
assert!(response.len() > FuseOutHeader::SIZE);
}
#[test]
fn test_lookup_nonexistent() {
let (_temp, dispatcher) = setup_dispatcher();
let name = b"nonexistent\0";
let mut request = make_header(FuseOpcode::Lookup, 1, name.len());
request.extend_from_slice(name);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, -libc::ENOENT);
}
#[test]
fn test_lookup_existing() {
let (temp, dispatcher) = setup_dispatcher();
std::fs::write(temp.path().join("test.txt"), "hello").unwrap();
let name = b"test.txt\0";
let mut request = make_header(FuseOpcode::Lookup, 1, name.len());
request.extend_from_slice(name);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, 0);
}
#[test]
fn test_mkdir_and_rmdir() {
let (_temp, dispatcher) = setup_dispatcher();
let mkdir_in = FuseMkdirIn {
mode: 0o755,
umask: 0,
};
let name = b"testdir\0";
let mut request = make_header(FuseOpcode::Mkdir, 1, size_of::<FuseMkdirIn>() + name.len());
let mkdir_bytes = unsafe {
std::slice::from_raw_parts(
&mkdir_in as *const FuseMkdirIn as *const u8,
size_of::<FuseMkdirIn>(),
)
};
request.extend_from_slice(mkdir_bytes);
request.extend_from_slice(name);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, 0);
let mut request = make_header(FuseOpcode::Rmdir, 1, name.len());
request.extend_from_slice(name);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, 0);
}
#[test]
fn test_open_read_write_release() {
let (temp, dispatcher) = setup_dispatcher();
std::fs::write(temp.path().join("test.txt"), "initial").unwrap();
let name = b"test.txt\0";
let mut request = make_header(FuseOpcode::Lookup, 1, name.len());
request.extend_from_slice(name);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, 0);
let entry = unsafe {
std::ptr::read_unaligned(
(response.as_ptr() as *const u8).add(FuseOutHeader::SIZE) as *const FuseEntryOut
)
};
let inode = entry.nodeid;
let open_in = FuseOpenIn {
flags: libc::O_RDWR as u32,
unused: 0,
};
let mut request = make_header(FuseOpcode::Open, inode, size_of::<FuseOpenIn>());
let open_bytes = unsafe {
std::slice::from_raw_parts(
&open_in as *const FuseOpenIn as *const u8,
size_of::<FuseOpenIn>(),
)
};
request.extend_from_slice(open_bytes);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, 0);
let open_out = unsafe {
std::ptr::read_unaligned(
(response.as_ptr() as *const u8).add(FuseOutHeader::SIZE) as *const FuseOpenOut
)
};
let fh = open_out.fh;
let read_in = FuseReadIn {
fh,
offset: 0,
size: 100,
read_flags: 0,
lock_owner: 0,
flags: 0,
padding: 0,
};
let mut request = make_header(FuseOpcode::Read, inode, size_of::<FuseReadIn>());
let read_bytes = unsafe {
std::slice::from_raw_parts(
&read_in as *const FuseReadIn as *const u8,
size_of::<FuseReadIn>(),
)
};
request.extend_from_slice(read_bytes);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, 0);
let data = &response[FuseOutHeader::SIZE..];
assert_eq!(data, b"initial");
let release_in = FuseReleaseIn {
fh,
flags: 0,
release_flags: 0,
lock_owner: 0,
};
let mut request = make_header(FuseOpcode::Release, inode, size_of::<FuseReleaseIn>());
let release_bytes = unsafe {
std::slice::from_raw_parts(
&release_in as *const FuseReleaseIn as *const u8,
size_of::<FuseReleaseIn>(),
)
};
request.extend_from_slice(release_bytes);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, 0);
}
#[test]
fn test_statfs() {
let (_temp, dispatcher) = setup_dispatcher();
let request = make_header(FuseOpcode::Statfs, 1, 0);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, 0);
assert!(response.len() >= FuseOutHeader::SIZE + size_of::<FuseStatfsOut>());
}
#[test]
fn test_unknown_opcode() {
let (_temp, dispatcher) = setup_dispatcher();
let header = FuseInHeader {
len: FuseInHeader::SIZE as u32,
opcode: 9999, unique: 1,
nodeid: 1,
uid: 0,
gid: 0,
pid: 0,
padding: 0,
};
let request = unsafe {
std::slice::from_raw_parts(
&header as *const FuseInHeader as *const u8,
FuseInHeader::SIZE,
)
};
let result = dispatcher.dispatch(request);
assert!(result.is_err());
}
#[test]
fn test_unsupported_opcode() {
let (_temp, dispatcher) = setup_dispatcher();
let request = make_header(FuseOpcode::Ioctl, 1, 0);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, -libc::ENOSYS);
}
#[test]
fn test_opendir_readdir_releasedir() {
let (temp, dispatcher) = setup_dispatcher();
std::fs::write(temp.path().join("file1.txt"), "").unwrap();
std::fs::write(temp.path().join("file2.txt"), "").unwrap();
let open_in = FuseOpenIn {
flags: 0,
unused: 0,
};
let mut request = make_header(FuseOpcode::Opendir, 1, size_of::<FuseOpenIn>());
let open_bytes = unsafe {
std::slice::from_raw_parts(
&open_in as *const FuseOpenIn as *const u8,
size_of::<FuseOpenIn>(),
)
};
request.extend_from_slice(open_bytes);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, 0);
let open_out = unsafe {
std::ptr::read_unaligned(
(response.as_ptr() as *const u8).add(FuseOutHeader::SIZE) as *const FuseOpenOut
)
};
let fh = open_out.fh;
let read_in = FuseReadIn {
fh,
offset: 0,
size: 4096,
read_flags: 0,
lock_owner: 0,
flags: 0,
padding: 0,
};
let mut request = make_header(FuseOpcode::Readdir, 1, size_of::<FuseReadIn>());
let read_bytes = unsafe {
std::slice::from_raw_parts(
&read_in as *const FuseReadIn as *const u8,
size_of::<FuseReadIn>(),
)
};
request.extend_from_slice(read_bytes);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, 0);
{
let body = &response[FuseOutHeader::SIZE..];
let base_offset: u64 = read_in.offset + 1; let mut pos = 0usize;
let mut i = 0usize;
while pos + size_of::<FuseDirent>() <= body.len() {
let dirent =
unsafe { std::ptr::read_unaligned(body[pos..].as_ptr() as *const FuseDirent) };
assert_eq!(
dirent.off,
base_offset + i as u64,
"dirent[{i}].off should be base_offset({base_offset}) + {i}"
);
let entry_size = FuseDirent::size(dirent.namelen as usize);
pos += entry_size;
i += 1;
}
assert!(i > 0, "should have parsed at least one dirent entry");
}
let release_in = FuseReleaseIn {
fh,
flags: 0,
release_flags: 0,
lock_owner: 0,
};
let mut request = make_header(FuseOpcode::Releasedir, 1, size_of::<FuseReleaseIn>());
let release_bytes = unsafe {
std::slice::from_raw_parts(
&release_in as *const FuseReleaseIn as *const u8,
size_of::<FuseReleaseIn>(),
)
};
request.extend_from_slice(release_bytes);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, 0);
}
#[test]
fn test_response_builder() {
let mut builder = ResponseBuilder::new();
builder.write_error(123, libc::ENOENT);
let response = builder.as_bytes();
let header = parse_response_header(response);
assert_eq!(header.unique, 123);
assert_eq!(header.error, -libc::ENOENT);
assert_eq!(header.len as usize, FuseOutHeader::SIZE);
builder.write_empty(456);
let response = builder.as_bytes();
let header = parse_response_header(response);
assert_eq!(header.unique, 456);
assert_eq!(header.error, 0);
assert_eq!(header.len as usize, FuseOutHeader::SIZE);
builder.write_bytes(789, b"hello");
let response = builder.as_bytes();
let header = parse_response_header(response);
assert_eq!(header.unique, 789);
assert_eq!(header.error, 0);
assert_eq!(header.len as usize, FuseOutHeader::SIZE + 5);
assert_eq!(&response[FuseOutHeader::SIZE..], b"hello");
}
#[test]
fn test_readdirplus() {
let (temp, dispatcher) = setup_dispatcher();
std::fs::write(temp.path().join("alpha.txt"), "aaa").unwrap();
std::fs::write(temp.path().join("beta.txt"), "bb").unwrap();
let open_in = FuseOpenIn {
flags: 0,
unused: 0,
};
let mut request = make_header(FuseOpcode::Opendir, 1, size_of::<FuseOpenIn>());
let open_bytes = unsafe {
std::slice::from_raw_parts(
&open_in as *const FuseOpenIn as *const u8,
size_of::<FuseOpenIn>(),
)
};
request.extend_from_slice(open_bytes);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, 0);
let open_out = unsafe {
std::ptr::read_unaligned(
(response.as_ptr() as *const u8).add(FuseOutHeader::SIZE) as *const FuseOpenOut
)
};
let fh = open_out.fh;
let read_in = FuseReadIn {
fh,
offset: 0,
size: 8192, read_flags: 0,
lock_owner: 0,
flags: 0,
padding: 0,
};
let mut request = make_header(FuseOpcode::Readdirplus, 1, size_of::<FuseReadIn>());
let read_bytes = unsafe {
std::slice::from_raw_parts(
&read_in as *const FuseReadIn as *const u8,
size_of::<FuseReadIn>(),
)
};
request.extend_from_slice(read_bytes);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, 0, "READDIRPLUS should succeed");
let body_len = response.len() - FuseOutHeader::SIZE;
assert!(
body_len > 0,
"READDIRPLUS response should contain directory entries"
);
let body = &response[FuseOutHeader::SIZE..];
assert!(
body.len() >= size_of::<FuseEntryOut>() + size_of::<FuseDirent>(),
"Response should contain at least one full READDIRPLUS entry"
);
let first_entry = unsafe { std::ptr::read_unaligned(body.as_ptr() as *const FuseEntryOut) };
assert!(
first_entry.nodeid > 0,
"First entry should have a valid node ID"
);
assert!(
first_entry.entry_valid > 0 || first_entry.attr_valid > 0,
"First entry should have cache timeouts set"
);
let release_in = FuseReleaseIn {
fh,
flags: 0,
release_flags: 0,
lock_owner: 0,
};
let mut request = make_header(FuseOpcode::Releasedir, 1, size_of::<FuseReleaseIn>());
let release_bytes = unsafe {
std::slice::from_raw_parts(
&release_in as *const FuseReleaseIn as *const u8,
size_of::<FuseReleaseIn>(),
)
};
request.extend_from_slice(release_bytes);
let response = dispatcher.dispatch(&request).unwrap();
let header = parse_response_header(&response);
assert_eq!(header.error, 0);
}
#[test]
fn test_setup_mapping_sentinel_fh() {
use crate::fuse::{FUSE_NO_FH, FuseSetupMappingIn};
use std::sync::atomic::{AtomicBool, Ordering};
struct RecordingMapper {
called: AtomicBool,
}
impl crate::DaxMapper for RecordingMapper {
fn setup_mapping(
&self,
_host_fd: i32,
_file_offset: u64,
_window_offset: u64,
_length: u64,
_writable: bool,
) -> std::result::Result<(), i32> {
self.called.store(true, Ordering::SeqCst);
Ok(())
}
fn remove_mapping(
&self,
_window_offset: u64,
_length: u64,
) -> std::result::Result<(), i32> {
Ok(())
}
}
let temp = tempfile::TempDir::new().unwrap();
std::fs::write(temp.path().join("exec_bin"), b"ELF_PAYLOAD").unwrap();
let fs = Arc::new(PassthroughFs::new(temp.path()).unwrap());
let mapper = Arc::new(RecordingMapper {
called: AtomicBool::new(false),
});
let mut dispatcher = FuseDispatcher::new(Arc::clone(&fs), DispatcherConfig::default());
dispatcher.set_dax_mapper(Arc::clone(&mapper) as Arc<dyn crate::DaxMapper>);
let name = b"exec_bin\0";
let mut req = make_header(FuseOpcode::Lookup, 1, name.len());
req.extend_from_slice(name);
let resp = dispatcher.dispatch(&req).unwrap();
let resp_hdr = parse_response_header(&resp);
assert_eq!(resp_hdr.error, 0, "lookup must succeed");
let entry_out = unsafe {
std::ptr::read_unaligned(
(resp.as_ptr() as *const u8).add(FuseOutHeader::SIZE) as *const FuseEntryOut
)
};
let inode = entry_out.nodeid;
let mapping_in = FuseSetupMappingIn {
fh: FUSE_NO_FH,
foffset: 0,
len: 4096,
flags: 0, moffset: 0,
};
let mut req = make_header(
FuseOpcode::SetupMapping,
inode,
size_of::<FuseSetupMappingIn>(),
);
let mapping_bytes = unsafe {
std::slice::from_raw_parts(
&mapping_in as *const FuseSetupMappingIn as *const u8,
size_of::<FuseSetupMappingIn>(),
)
};
req.extend_from_slice(mapping_bytes);
let resp = dispatcher.dispatch(&req).unwrap();
let resp_hdr = parse_response_header(&resp);
assert_eq!(
resp_hdr.error, 0,
"SETUPMAPPING with FUSE_NO_FH sentinel should succeed (got errno {})",
-resp_hdr.error
);
assert!(
mapper.called.load(Ordering::SeqCst),
"DaxMapper::setup_mapping should have been called via the sentinel-fh path"
);
}
#[test]
fn test_dax_fs_ext_open_inode_for_dax_reads_content() {
use crate::dispatcher::DaxFsExt;
use std::io::Read;
let temp = tempfile::TempDir::new().unwrap();
std::fs::write(temp.path().join("data.bin"), b"hello dax").unwrap();
let fs = PassthroughFs::new(temp.path()).unwrap();
let name = std::ffi::OsStr::new("data.bin");
let (inode, _attr) = fs.lookup(1, name).unwrap();
let mut file = DaxFsExt::open_inode_for_dax(&fs, inode, false).unwrap();
let mut buf = Vec::new();
file.read_to_end(&mut buf).unwrap();
assert_eq!(
buf, b"hello dax",
"open_inode_for_dax should expose file content"
);
}
#[test]
fn test_dax_fs_ext_open_inode_for_dax_toctou_rename_detected() {
use crate::dispatcher::DaxFsExt;
let temp = tempfile::TempDir::new().unwrap();
std::fs::write(temp.path().join("original.bin"), b"orig").unwrap();
std::fs::write(temp.path().join("replacement.bin"), b"evil").unwrap();
let fs = PassthroughFs::new(temp.path()).unwrap();
let name = std::ffi::OsStr::new("original.bin");
let (inode, _attr) = fs.lookup(1, name).unwrap();
std::fs::rename(
temp.path().join("replacement.bin"),
temp.path().join("original.bin"),
)
.unwrap();
let result = DaxFsExt::open_inode_for_dax(&fs, inode, false);
let err = result.expect_err("should fail with EIO after TOCTOU swap");
assert_eq!(
err.raw_os_error(),
Some(libc::EIO),
"expected EIO for TOCTOU-detected rename, got: {err}"
);
}