use std::{ cmp::min, collections::HashMap, ffi::{OsStr, OsString}, iter::repeat, time::{Duration, SystemTime} }; use fuser::{consts::{FOPEN_DIRECT_IO, FOPEN_KEEP_CACHE, FUSE_WRITEBACK_CACHE}, FileAttr, FileType, Filesystem, TimeOrNow}; use nix::{ libc::{ EBADF, EBUSY, EEXIST, EINVAL, EISDIR, ENOENT, ENOTDIR, EPERM, O_DIRECT, O_RDONLY, SEEK_SET }, unistd::{getgid, getuid}, }; const TTL: Duration = Duration::from_secs(1); const BLOCK_SIZE: u32 = 512; const ROOT_INODE: u64 = 1; struct OpenFile { ino: u64, flags: i32, } enum OpenFd { File(OpenFile), Directory(OpenFile), } struct File { ino: u64, kind: FileType, data: Vec, mode: u32, crtime: SystemTime, atime: SystemTime, mtime: SystemTime, ctime: SystemTime, } impl File { fn as_file_attr(&self) -> FileAttr { let nlink = if self.ino == ROOT_INODE { 2 } else { 1 }; let attr = FileAttr { ino: self.ino, size: self.data.len().try_into().unwrap(), blocks: block_count(self.data.len()), atime: self.atime, mtime: self.mtime, ctime: self.ctime, kind: self.kind, crtime: self.crtime, perm: self.mode as u16, nlink, uid: getuid().as_raw(), gid: getgid().as_raw(), rdev: 0, blksize: BLOCK_SIZE, flags: 0, }; debug!("attr: {:?}", attr); attr } } pub struct CorruptFs { ino_counter: u64, fh_counter: u64, files: HashMap, // inode -> File inode_map: HashMap, // name -> inode open_fds: HashMap, // fh -> OpenFd allow_direct_io: bool, } impl CorruptFs { pub fn new(allow_direct_io: bool) -> Self { let mut fs = Self { ino_counter: 2, fh_counter: 1, files: HashMap::new(), inode_map: HashMap::new(), open_fds: HashMap::new(), allow_direct_io, }; let now = SystemTime::now(); let rootdir = File { ino: ROOT_INODE, kind: FileType::Directory, data: Vec::new(), mode: 0o755, crtime: now, atime: now, mtime: now, ctime: now, }; fs.inode_map.insert(OsString::from("."), rootdir.ino); fs.inode_map.insert(OsString::from(".."), rootdir.ino); fs.files.insert(rootdir.ino, rootdir); fs } fn next_inode(&mut self) -> u64 { let ino = self.ino_counter; self.ino_counter += 1; ino } fn next_fh(&mut self) -> u64 { let fh = self.fh_counter; self.fh_counter += 1; fh } fn open_file(&self, fh: u64) -> Option<&OpenFile> { self.open_fds.get(&fh).and_then(|open_fd| match open_fd { OpenFd::File(open_file) => Some(open_file), _ => None, }) } fn open_file_with_file_mut(&mut self, fh: u64) -> Option<(&mut File, &OpenFile)> { self.open_fds .get(&fh) .and_then(|open_fd| match open_fd { OpenFd::File(open_file) => Some(open_file), _ => None, }) .and_then(|open_file| { self.files .get_mut(&open_file.ino) .map(|file| (file, open_file)) }) } fn is_open_dir(&self, fh: u64) -> bool { self.open_fds .get(&fh) .map(|open_fd| match open_fd { OpenFd::Directory(_) => true, _ => false, }) .unwrap_or(false) } } impl Filesystem for CorruptFs { fn init(&mut self, _req: &fuser::Request<'_>, config: &mut fuser::KernelConfig) -> Result<(), nix::libc::c_int> { if let Err(unsup) = config.add_capabilities(FUSE_WRITEBACK_CACHE) { warn!("Unsupported kernel caps: 0x{:08x}", unsup); } Ok(()) } fn statfs(&mut self, _req: &fuser::Request<'_>, _ino: u64, reply: fuser::ReplyStatfs) { reply.statfs( u64::MAX, u64::MAX, u64::MAX, self.files.len() as u64, u64::MAX, BLOCK_SIZE, 4096, BLOCK_SIZE, ); } fn lookup( &mut self, req: &fuser::Request<'_>, parent: u64, name: &OsStr, reply: fuser::ReplyEntry, ) { if req.uid() != getuid().as_raw() { reply.error(EPERM); } else if parent != ROOT_INODE { reply.error(ENOENT); } else if let Some(file) = self .inode_map .get(&name.to_os_string()) .and_then(|inode| self.files.get(inode)) { reply.entry(&TTL, &file.as_file_attr(), 0); } else { reply.error(ENOENT); } } fn getattr(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) { debug!("getattr"); if let Some(file) = self.files.get(&ino) { reply.attr(&TTL, &file.as_file_attr()); } else { reply.error(ENOENT); } } fn access(&mut self, req: &fuser::Request<'_>, ino: u64, _mask: i32, reply: fuser::ReplyEmpty) { if req.uid() != getuid().as_raw() { reply.error(EPERM); } else if self.files.contains_key(&ino) { debug!("returning OK to access request"); reply.ok(); } else { reply.error(ENOENT); } } fn setattr( &mut self, req: &fuser::Request<'_>, ino: u64, mode: Option, uid: Option, gid: Option, size: Option, atime: Option, mtime: Option, ctime: Option, fh: Option, crtime: Option, _chgtime: Option, _bkuptime: Option, _flags: Option, reply: fuser::ReplyAttr, ) { if req.uid() != getuid().as_raw() { reply.error(EPERM); } else if let Some((file, _)) = fh.and_then(|fh| self.open_file_with_file_mut(fh)) { do_setattr(file, mode, uid, gid, size, atime, mtime, ctime, crtime, reply); } else if let Some(file) = self.files.get_mut(&ino) { do_setattr(file, mode, uid, gid, size, atime, mtime, ctime, crtime, reply); } else { reply.error(ENOENT); } } fn open(&mut self, req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) { if req.uid() != getuid().as_raw() { reply.error(EPERM); } else if (flags & O_DIRECT) != 0 && !self.allow_direct_io { reply.error(EINVAL); } else if self.files.contains_key(&ino) { let fh = self.next_fh(); let open_file = OpenFile { ino, flags }; self.open_fds.insert(fh, OpenFd::File(open_file)); let o_flags = if (flags & O_DIRECT) != 0 { FOPEN_DIRECT_IO } else { FOPEN_KEEP_CACHE }; reply.opened(fh, o_flags); } else { reply.error(ENOENT); } } fn create( &mut self, req: &fuser::Request<'_>, _parent: u64, name: &OsStr, mode: u32, _umask: u32, flags: i32, reply: fuser::ReplyCreate, ) { if req.uid() != getuid().as_raw() { reply.error(EPERM); } else if self.inode_map.contains_key(&name.to_os_string()) { reply.error(EEXIST); } else if (flags & O_DIRECT) != 0 && !self.allow_direct_io { reply.error(EINVAL); } else { debug!("create with mode {:o}", mode); let now = SystemTime::now(); let file = File { ino: self.next_inode(), kind: FileType::RegularFile, data: Vec::new(), mode, crtime: now, atime: now, mtime: now, ctime: now, }; let attr = file.as_file_attr(); self.inode_map.insert(name.to_os_string(), file.ino); self.files.insert(file.ino, file); let fh = self.next_fh(); let open_file = OpenFile { ino: attr.ino, flags, }; self.open_fds.insert(fh, OpenFd::File(open_file)); let cr_flags = if (flags & O_DIRECT) != 0 { FOPEN_DIRECT_IO } else { FOPEN_KEEP_CACHE }; reply.created(&TTL, &attr, 0, fh, cr_flags); } } fn lseek( &mut self, _req: &fuser::Request<'_>, _ino: u64, fh: u64, offset: i64, whence: i32, reply: fuser::ReplyLseek, ) { if let Some((file, _)) = self.open_file_with_file_mut(fh) { if whence == SEEK_SET { if (offset as usize) < file.data.len() { reply.offset(offset); } else { reply.error(EINVAL); } } else { reply.error(EINVAL); } } else { reply.error(EBADF); } } fn read( &mut self, _req: &fuser::Request<'_>, _ino: u64, fh: u64, offset: i64, size: u32, _flags: i32, _lock_owner: Option, reply: fuser::ReplyData, ) { if let Some(file) = self .open_file(fh) .and_then(|open_file| self.files.get(&open_file.ino)) { if offset as usize >= file.data.len() { reply.error(EINVAL); } else { let bytes = min(size as usize, file.data.len() - (offset as usize)) as usize; let data = std::iter::repeat_with(|| rand::random()); reply.data(&data.take(bytes).collect::>()); //reply.data(&file.data[(offset as usize)..bytes]); } } else { reply.error(EBADF); } } fn write( &mut self, _req: &fuser::Request<'_>, _ino: u64, fh: u64, offset: i64, data: &[u8], _write_flags: u32, _flags: i32, _lock_owner: Option, reply: fuser::ReplyWrite, ) { if let Some((file, open_file)) = self.open_file_with_file_mut(fh) { if (open_file.flags & O_RDONLY) != 0 { reply.error(EBADF); } else { let offset = offset as usize; if offset == file.data.len() { file.data.extend_from_slice(data); reply.written(data.len() as u32); } else if offset > file.data.len() { file.data.extend(repeat(0).take(offset - file.data.len())); file.data.extend_from_slice(data); reply.written(data.len() as u32); } else { let range = offset..min(file.data.len(), offset + data.len()); file.data.splice(range, data.iter().map(|v| *v)); reply.written(data.len() as u32); } } } else { reply.error(EBADF); } } fn flush( &mut self, _req: &fuser::Request<'_>, _ino: u64, fh: u64, _lock_owner: u64, reply: fuser::ReplyEmpty, ) { if let Some(open_file) = self.open_file(fh) { if (open_file.flags & O_RDONLY) != 0 { reply.error(EBADF); } else { reply.ok(); } } else { reply.error(EBADF); } } fn release( &mut self, _req: &fuser::Request<'_>, _ino: u64, fh: u64, _flags: i32, _lock_owner: Option, _flush: bool, reply: fuser::ReplyEmpty, ) { if let Some(open_fd) = self.open_fds.remove(&fh) { match open_fd { OpenFd::File(_) => reply.ok(), OpenFd::Directory(open_dir) => { self.open_fds.insert(fh, OpenFd::Directory(open_dir)); reply.error(EBADF); } } } else { reply.error(EBADF); } } fn opendir(&mut self, req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) { if req.uid() != getuid().as_raw() { debug!("opendir: bad user"); reply.error(EPERM); } else if ino != ROOT_INODE { debug!("opendir: bad ino"); if self.files.contains_key(&ino) { reply.error(ENOTDIR); } else { reply.error(ENOENT); } } else { debug!("opendir: ok"); let fh = self.next_fh(); let open_file = OpenFile { ino: ROOT_INODE, flags, }; self.open_fds.insert(fh, OpenFd::Directory(open_file)); reply.opened(fh, 0); } } fn readdir( &mut self, _req: &fuser::Request<'_>, _ino: u64, fh: u64, offset: i64, mut reply: fuser::ReplyDirectory, ) { if self.is_open_dir(fh) { for (i, (name, inode)) in self.inode_map.iter().skip(offset as usize).enumerate() { let file = self.files.get(inode).unwrap(); debug!("readdir: adding file {:?}", name); if reply.add(*inode, (i + 1) as i64, file.kind, name) { break; } } reply.ok(); } else { reply.error(EBADF); } } fn readdirplus( &mut self, _req: &fuser::Request<'_>, _ino: u64, fh: u64, offset: i64, mut reply: fuser::ReplyDirectoryPlus, ) { if self.is_open_dir(fh) { for (i, (name, inode)) in self.inode_map.iter().skip(offset as usize).enumerate() { let file = self.files.get(inode).unwrap(); debug!("readdirplus: adding file {:?}", name); if reply.add(*inode, (i + 1) as i64, name, &TTL, &file.as_file_attr(), 0) { break; } } reply.ok(); } else { reply.error(EBADF); } } fn releasedir( &mut self, _req: &fuser::Request<'_>, _ino: u64, fh: u64, _flags: i32, reply: fuser::ReplyEmpty, ) { if let Some(open_fd) = self.open_fds.remove(&fh) { match open_fd { OpenFd::Directory(_) => reply.ok(), OpenFd::File(open_file) => { self.open_fds.insert(fh, OpenFd::File(open_file)); reply.error(EBADF); } } } else { reply.error(EBADF); } } fn fsync( &mut self, _req: &fuser::Request<'_>, _ino: u64, _fh: u64, _datasync: bool, reply: fuser::ReplyEmpty, ) { reply.ok(); } fn fsyncdir( &mut self, _req: &fuser::Request<'_>, _ino: u64, _fh: u64, _datasync: bool, reply: fuser::ReplyEmpty, ) { reply.ok(); } fn unlink( &mut self, _req: &fuser::Request<'_>, parent: u64, name: &OsStr, reply: fuser::ReplyEmpty, ) { if parent != ROOT_INODE { reply.error(ENOENT); } else if let Some(file) = self .inode_map .get(&name.to_os_string()) .and_then(|ino| self.files.get(ino)) { if file.ino == ROOT_INODE { reply.error(EISDIR); } else if file.kind == FileType::RegularFile { let ino = file.ino; self.inode_map.remove(&name.to_os_string()); self.files.remove(&ino); reply.ok(); } else { reply.error(EPERM); } } else { reply.error(ENOENT); } } fn rmdir( &mut self, _req: &fuser::Request<'_>, _parent: u64, name: &OsStr, reply: fuser::ReplyEmpty, ) { if name == "." { reply.error(EBUSY); } else { reply.error(ENOENT); } } fn rename( &mut self, req: &fuser::Request<'_>, parent: u64, name: &OsStr, newparent: u64, newname: &OsStr, _flags: u32, reply: fuser::ReplyEmpty, ) { if req.uid() != getuid().as_raw() { reply.error(EPERM); } else if parent != ROOT_INODE || newparent != ROOT_INODE { reply.error(ENOENT); } else if let Some(file) = self .inode_map .get(&name.to_os_string()) .and_then(|ino| self.files.get(ino)) { if file.ino == ROOT_INODE { reply.error(EBUSY); } else if file.kind == FileType::RegularFile { self.inode_map.remove(&name.to_os_string()); self.inode_map.insert(newname.to_os_string(), file.ino); reply.ok(); } else { reply.error(EPERM); } } else { reply.error(ENOENT); } } } fn block_count(size: usize) -> u64 { let rem = if size % (BLOCK_SIZE as usize) != 0 { 1 } else { 0 }; (size / (BLOCK_SIZE as usize) + rem).try_into().unwrap() } fn do_setattr( file: &mut File, mode: Option, uid: Option, gid: Option, size: Option, atime: Option, mtime: Option, ctime: Option, crtime: Option, reply: fuser::ReplyAttr, ) { if uid.filter(|uid| *uid != getuid().as_raw()).is_some() || gid.filter(|gid| *gid != getgid().as_raw()).is_some() { reply.error(EPERM); } else { if let Some(mode) = mode { file.mode = mode; } if let Some(size) = size { let size = size as usize; if file.data.len() < size { file.data.extend(repeat(0).take(size - file.data.len())) } else if file.data.len() > size { file.data.truncate(size as usize); } } if let Some(atime) = parse_timeornow(atime) { file.atime = atime; } if let Some(mtime) = parse_timeornow(mtime) { file.mtime = mtime; } if let Some(ctime) = ctime { file.ctime = ctime; } if let Some(crtime) = crtime { file.crtime = crtime; } reply.attr(&TTL, &file.as_file_attr()); } } fn parse_timeornow(t: Option) -> Option { t.map(|t| match t { TimeOrNow::Now => SystemTime::now(), TimeOrNow::SpecificTime(st) => st, }) }