From e323b4a675eda402581c093d3dfd342162d20e37 Mon Sep 17 00:00:00 2001 From: Sergei Shulepov Date: Tue, 14 Jan 2025 13:16:31 +0000 Subject: [PATCH] torture: trickfs Introduces trickfs, a fuse filesystem for made for failure injection. --- Cargo.lock | 128 +++++- Cargo.toml | 2 +- trickfs/Cargo.toml | 15 + trickfs/README.md | 13 + trickfs/src/lib.rs | 1012 +++++++++++++++++++++++++++++++++++++++++++ trickfs/src/main.rs | 9 + 6 files changed, 1161 insertions(+), 18 deletions(-) create mode 100644 trickfs/Cargo.toml create mode 100644 trickfs/README.md create mode 100644 trickfs/src/lib.rs create mode 100644 trickfs/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 2566c069..5608899e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,7 +27,7 @@ dependencies = [ "getrandom", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.34", ] [[package]] @@ -570,6 +570,16 @@ dependencies = [ "syn", ] +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.8.4" @@ -580,6 +590,19 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -588,9 +611,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys", @@ -598,9 +621,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "funty" @@ -608,6 +631,22 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "fuser" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53274f494609e77794b627b1a3cddfe45d675a6b2e9ba9c0fdc8d8eee2184369" +dependencies = [ + "libc", + "log", + "memchr", + "nix", + "page_size", + "pkg-config", + "smallvec", + "zerocopy 0.8.14", +] + [[package]] name = "futures" version = "0.3.31" @@ -732,9 +771,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -802,6 +841,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "imbl" version = "3.0.0" @@ -891,9 +936,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.168" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libfuzzer-sys" @@ -924,9 +969,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "loom" @@ -1112,6 +1157,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -1167,6 +1222,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "plotters" version = "0.3.6" @@ -1248,7 +1309,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ - "env_logger", + "env_logger 0.8.4", "log", "rand", ] @@ -1421,9 +1482,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ "bitflags 2.5.0", "errno", @@ -1597,12 +1658,14 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.10.1" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", + "getrandom", + "once_cell", "rustix", "windows-sys", ] @@ -1818,6 +1881,17 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "trickfs" +version = "0.1.0" +dependencies = [ + "env_logger 0.11.6", + "fuser", + "libc", + "log", + "tempfile", +] + [[package]] name = "typenum" version = "1.17.0" @@ -2102,7 +2176,16 @@ version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.7.34", +] + +[[package]] +name = "zerocopy" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +dependencies = [ + "zerocopy-derive 0.8.14", ] [[package]] @@ -2115,3 +2198,14 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zerocopy-derive" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 5be606a9..505cf7fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["core", "nomt", "fuzz", "torture", "examples/*"] +members = ["core", "nomt", "fuzz", "torture", "examples/*", "trickfs"] exclude = ["benchtop"] [workspace.package] diff --git a/trickfs/Cargo.toml b/trickfs/Cargo.toml new file mode 100644 index 00000000..1d771d0f --- /dev/null +++ b/trickfs/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "trickfs" +version = "0.1.0" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +env_logger = "0.11.6" +fuser = { version = "0.15.1", features = ["abi-7-23"] } +libc = "0.2.169" +log = "0.4.22" +tempfile = "3.15.0" diff --git a/trickfs/README.md b/trickfs/README.md new file mode 100644 index 00000000..e4c6fa31 --- /dev/null +++ b/trickfs/README.md @@ -0,0 +1,13 @@ +# trickfs + +A FUSE filesystem useful for failure injection. + +# Building + +Building the project requires fuse3 and fuse to be available. On Ubuntu, you can install them with +the following commands: + +```sh +sudo apt update +sudo apt install libfuse3-dev libfuse-dev +``` diff --git a/trickfs/src/lib.rs b/trickfs/src/lib.rs new file mode 100644 index 00000000..9602c4d9 --- /dev/null +++ b/trickfs/src/lib.rs @@ -0,0 +1,1012 @@ +use std::{ + collections::BTreeMap, + ffi::{OsStr, OsString}, + fmt, + path::Path, + sync::LazyLock, + time::{Duration, UNIX_EPOCH}, + u64, +}; + +use fuser::FileAttr; + +const DEFAULT_TTL: Duration = Duration::from_secs(1); +const BLK_SIZE: u64 = 512; +const MAX_FILE_SIZE: u64 = 1 << 40; + +static DOT: LazyLock = LazyLock::new(|| OsString::from(".")); +static DOTDOT: LazyLock = LazyLock::new(|| OsString::from("..")); + +struct MmapStorage { + ptr: *mut u8, +} + +impl MmapStorage { + pub fn new() -> MmapStorage { + let ptr = unsafe { + libc::mmap( + std::ptr::null_mut(), + MAX_FILE_SIZE as usize, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_NORESERVE, + -1, + 0, + ) + }; + if ptr == libc::MAP_FAILED { + panic!("mmap failed"); + } + MmapStorage { ptr: ptr.cast() } + } + + // SAFETY: this is safe because we know that `ptr` points to a valid memory region of + // `MAX_FILE_SIZE` bytes. The lifetime and mutability of the slice is tied to the + // lifetime and mutability of `self`. u8 does not impose any alignment requirements. + // + // One safety note here is that `ptr` is allocated with `MAP_NORESERVE` flag, so + // the memory is only "committed" upon the first access and if there is not actual + // physical memory to back it up, the kernel will kill the process. + // + // This is not considered a problem since a similiar thing can happen on any allocation + // path (e.g. `Vec::push`). + + /// Returns the underlying memory as a slice. + pub fn as_slice(&self) -> &[u8] { + // SAFETY: see the note above. + unsafe { std::slice::from_raw_parts(self.ptr, MAX_FILE_SIZE as usize) } + } + + /// Returns the underlying memory as a mutable slice. + pub fn as_slice_mut(&mut self) -> &mut [u8] { + // SAFETY: see the note above. + unsafe { std::slice::from_raw_parts_mut(self.ptr, MAX_FILE_SIZE as usize) } + } +} + +impl Drop for MmapStorage { + fn drop(&mut self) { + unsafe { + // SAFETY: we know that `ptr` points to a valid memory region of `MAX_FILE_SIZE` bytes + // previously allocated by `mmap`. + libc::munmap(self.ptr.cast(), MAX_FILE_SIZE as usize); + } + } +} + +// Safety: MmapStorage is just a chunk of memory, similiar to a Vec<_>, so it is safe to send +// across threads. +unsafe impl Send for MmapStorage {} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct Inode(u64); + +impl Inode { + #[allow(dead_code)] + const INVALID: Inode = Inode(0); + const ROOT: Inode = Inode(1); +} + +pub struct InodeData { + parent: Inode, + generation: u64, + kind: fuser::FileType, + file_size: u64, + storage: Option, +} + +impl InodeData { + pub fn new_file(parent: Inode) -> Self { + InodeData { + parent, + generation: 0, + kind: fuser::FileType::RegularFile, + file_size: 0, + storage: None, + } + } + + pub fn new_dir(parent: Inode) -> Self { + InodeData { + parent, + generation: 0, + kind: fuser::FileType::Directory, + file_size: 0, + storage: None, + } + } + + /// Returns the inode containing this one. + pub fn parent(&self) -> Inode { + self.parent + } + + /// Return the number of blocks in this file or directory. + pub fn blocks(&self) -> u64 { + // Derive the number of blocks from the file size. + (self.file_size + BLK_SIZE - 1) / BLK_SIZE + } + + /// Returns the size of the file in bytes. + pub fn size(&self) -> u64 { + self.file_size + } + + /// Returns the kind of this inode. + pub fn kind(&self) -> fuser::FileType { + self.kind + } + + pub fn is_dir(&self) -> bool { + self.kind() == fuser::FileType::Directory + } + + pub fn perm(&self) -> u16 { + if self.kind() == fuser::FileType::Directory { + 0o755 + } else { + 0o644 + } + } + + pub fn mk_file_attrs(&self, ino: Inode) -> FileAttr { + FileAttr { + ino: ino.0, + size: self.size(), + blocks: self.blocks(), + atime: UNIX_EPOCH, + mtime: UNIX_EPOCH, + ctime: UNIX_EPOCH, + crtime: UNIX_EPOCH, + kind: self.kind(), + perm: self.perm(), + nlink: 1, + uid: 0, + gid: 0, + blksize: BLK_SIZE as u32, + rdev: 0, + flags: 0, + } + } + + pub fn content(&self) -> &[u8] { + if self.storage.is_none() { + return &[]; + } + self.storage.as_ref().unwrap().as_slice() + } + + pub fn content_mut(&mut self) -> &mut [u8] { + if self.storage.is_none() { + self.storage = Some(MmapStorage::new()); + } + self.storage.as_mut().unwrap().as_slice_mut() + } + + fn set_size(&mut self, new_file_sz: u64) { + self.file_size = new_file_sz; + } +} + +impl fmt::Debug for InodeData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "InodeData {{ parent: {:?}, generation: {}, kind: {:?}, file_size: {} }}", + self.parent, self.generation, self.kind, self.file_size + ) + } +} + +struct Container { + names: Vec, + inos: Vec, +} + +impl Container { + fn new() -> Self { + Container { + names: Vec::new(), + inos: Vec::new(), + } + } + + fn lookup_by_name(&self, name: &OsStr) -> Option { + for (i, n) in self.names.iter().enumerate() { + if n == name { + return Some(self.inos[i]); + } + } + None + } + + fn lookup_by_inode(&self, ino: Inode) -> Option<&OsStr> { + for (i, x) in self.inos.iter().enumerate() { + if *x == ino { + return Some(&self.names[i]); + } + } + None + } + + /// Removes the entry with the given name and removes its inode. + fn remove(&mut self, name: &OsStr) -> Option { + for i in 0..self.names.len() { + if self.names[i] == name { + let ino = self.inos.remove(i); + self.names.remove(i); + return Some(ino); + } + } + None + } + + fn register(&mut self, name: OsString, ino: Inode) { + self.names.push(name); + self.inos.push(ino); + } + + fn nth(&self, i: usize) -> Option<(&OsStr, Inode)> { + if i >= self.names.len() { + return None; + } + Some((&self.names[i], self.inos[i])) + } + + fn count(&self) -> usize { + self.names.len() + } + + fn iter(&self) -> ReadDir<'_> { + ReadDir { + container: self, + offset: 0, + } + } +} + +struct ReadDir<'c> { + container: &'c Container, + offset: usize, +} + +impl<'c> Iterator for ReadDir<'c> { + type Item = (&'c OsStr, Inode); + + fn next(&mut self) -> Option { + if self.offset >= self.container.count() { + return None; + } + let (name, ino) = self.container.nth(self.offset)?; + self.offset += 1; + Some((name, ino)) + } +} + +struct Tree { + ino_to_container: BTreeMap, +} + +impl Tree { + fn new() -> Self { + Tree { + ino_to_container: BTreeMap::new(), + } + } +} + +/// The implementation of the file system. +pub struct Trick { + tree: Tree, + /// Stored inodes. The first inode is the root directory. + /// + /// Note that inodes are 1-based and this vector is 0-based. + inodes: Vec, + freelist: Vec, +} + +impl Trick { + pub fn new() -> Self { + let tree = Tree::new(); + let inodes = Vec::new(); + let freelist = Vec::new(); + let mut fs = Trick { + tree, + inodes, + freelist, + }; + // Initialize the root directory. Parent of the ROOT is ROOT. + fs.register_inode(InodeData::new_dir(Inode::ROOT)); + fs.tree + .ino_to_container + .insert(Inode::ROOT, Container::new()); + fs + } + + fn lookup_inode(&self, ino: Inode) -> Option<&InodeData> { + let ino = ino.0; + if ino == 0 { + return None; + } + let inodes_index = usize::try_from(ino).unwrap() - 1; + self.inodes.get(inodes_index) + } + + fn lookup_inode_mut(&mut self, ino: Inode) -> Option<&mut InodeData> { + let ino = ino.0; + if ino == 0 { + return None; + } + let inodes_index = usize::try_from(ino).unwrap() - 1; + self.inodes.get_mut(inodes_index) + } + + fn register_inode(&mut self, mut inode: InodeData) -> Inode { + match self.freelist.pop() { + Some(ino) => { + let inodes_index = ino.0 as usize - 1; + // Since we are reusing the inode we should bump its generation number. + inode.generation = self.inodes[inodes_index].generation + 1; + self.inodes[inodes_index] = inode; + ino + } + None => { + let ino = Inode(self.inodes.len() as u64 + 1); + self.inodes.push(inode); + ino + } + } + } + + /// Marks the given inode as removed, prepares it for reusing. + fn remove_inode(&mut self, removed_ino: Inode) { + self.freelist.push(removed_ino); + } + + fn reconstruct_full_path(&self, ino: Inode) -> OsString { + let mut segments = Vec::::new(); + let mut ino = ino; + while ino != Inode::ROOT { + let parent_inode = self.lookup_inode(ino).unwrap().parent(); + let container = self.tree.ino_to_container.get(&parent_inode).unwrap(); + let name = container.lookup_by_inode(ino).unwrap(); + segments.push(name.to_os_string()); + ino = parent_inode; + } + let mut path = OsString::new(); + path.push("/"); + for segment in segments.iter().rev() { + path.push(segment); + path.push("/"); + } + path + } +} + +impl fuser::Filesystem for Trick { + fn lookup( + &mut self, + _req: &fuser::Request<'_>, + parent: u64, + name: &std::ffi::OsStr, + reply: fuser::ReplyEntry, + ) { + log::trace!("lookup: parent={}, name={:?}", parent, name); + let Some(parent) = self.tree.ino_to_container.get(&Inode(parent)) else { + log::trace!("parent inode doesn't exist"); + reply.error(libc::ENOENT); + return; + }; + let Some(ino) = parent.lookup_by_name(name) else { + log::trace!("file doesn't exist"); + reply.error(libc::ENOENT); + return; + }; + let Some(inode) = self.lookup_inode(ino) else { + log::error!("inode doesn't exist. This looks like a bug."); + reply.error(libc::ENOENT); + return; + }; + let file_attr = inode.mk_file_attrs(ino); + reply.entry(&DEFAULT_TTL, &file_attr, inode.generation); + } + + fn getattr( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + _fh: Option, + reply: fuser::ReplyAttr, + ) { + let ino = Inode(ino); + let Some(inode) = self.lookup_inode(ino) else { + log::error!("inode doesn't exist. This looks like a bug."); + reply.error(libc::ENOENT); + return; + }; + let file_attr = inode.mk_file_attrs(ino); + reply.attr(&DEFAULT_TTL, &file_attr); + } + + 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, + ) { + // trickfs doesn't track any times, so safely discard. + let _ = (atime, mtime, ctime, crtime, chgtime, bkuptime); + // discard those as well + let _ = (mode, uid, gid, fh, flags); + let ino = Inode(ino); + let Some(inode) = self.lookup_inode_mut(ino) else { + log::error!("inode doesn't exist. This looks like a bug."); + reply.error(libc::ENOENT); + return; + }; + if let Some(new_size) = size { + inode.set_size(new_size); + } + let file_attr = inode.mk_file_attrs(ino); + reply.attr(&DEFAULT_TTL, &file_attr); + } + + fn create( + &mut self, + _req: &fuser::Request<'_>, + parent: u64, + name: &OsStr, + mode: u32, + umask: u32, + flags: i32, + reply: fuser::ReplyCreate, + ) { + // we don't really care about these parameters. + let _ = (mode, umask, flags); + let Some(parent_container) = self.tree.ino_to_container.get(&Inode(parent)) else { + log::trace!("parent inode doesn't exist"); + reply.error(libc::ENOENT); + return; + }; + if parent_container.lookup_by_name(name).is_some() { + log::trace!("file already exists"); + reply.error(libc::EEXIST); + return; + } + let ino = self.register_inode(InodeData::new_file(Inode(parent))); + // unwrap: we just checked that the parent exists. + self.tree + .ino_to_container + .get_mut(&Inode(parent)) + .unwrap() + .register(name.to_os_string(), ino); + // unwrap: we just created this inode. + let inode = self.lookup_inode(ino).unwrap(); + let file_attr = inode.mk_file_attrs(ino); + reply.created(&DEFAULT_TTL, &file_attr, inode.generation, 0, 0); + } + + fn open(&mut self, _req: &fuser::Request<'_>, ino: u64, _flags: i32, reply: fuser::ReplyOpen) { + match self.lookup_inode(Inode(ino)) { + Some(_inode_data) => reply.opened(0, 0), + None => { + reply.error(libc::ENOENT); + } + } + } + + fn read( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + fh: u64, + offset: i64, + size: u32, + flags: i32, + lock_owner: Option, + reply: fuser::ReplyData, + ) { + let _ = (fh, flags, lock_owner); + let Some(inode_data) = self.lookup_inode(Inode(ino)) else { + reply.error(libc::ENOENT); + return; + }; + log::trace!( + "reading {:?} {:#?}", + self.reconstruct_full_path(Inode(ino)), + inode_data + ); + // TODO: Check the offset. + // TODO: O_DIRECT handling. + // + // If it is O_DIRECT we just need to serve the entire page. + let size = size as usize; + let offset = offset as usize; + let end = offset + size; + let content = &inode_data.content(); + if content.is_empty() { + // The backing buffer has not yet been created. Let's just return an empty buffer. + if has_odirect(flags) { + reply.data(&[0u8; 4096]); + } else { + reply.data(&[]); + } + return; + } + if end > content.len() { + reply.data(&content[offset..]); + } else { + reply.data(&content[offset..end]); + } + } + + 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, + ) { + let _ = (fh, flags, write_flags, lock_owner); + let Some(inode_data) = self.lookup_inode_mut(Inode(ino)) else { + reply.error(libc::ENOENT); + return; + }; + // TODO: this is definitely wrong wrt sign ext. + let offset = offset as usize; + let len = data.len(); + let new_file_sz = offset + len; + if new_file_sz > MAX_FILE_SIZE as usize { + reply.error(libc::EFBIG); + return; + } + if inode_data.size() < new_file_sz as u64 { + inode_data.set_size(new_file_sz as u64); + } + inode_data.content_mut()[offset..offset + len].copy_from_slice(data); + reply.written(len as u32); + } + + fn mkdir( + &mut self, + _req: &fuser::Request<'_>, + parent: u64, + name: &OsStr, + mode: u32, + umask: u32, + reply: fuser::ReplyEntry, + ) { + let _ = (mode, umask); + let Some(inode_data) = self.lookup_inode_mut(Inode(parent)) else { + reply.error(libc::ENOENT); + return; + }; + if !inode_data.is_dir() { + reply.error(libc::ENOTDIR); + return; + } + let ino = self.register_inode(InodeData::new_dir(Inode(parent))); + self.tree.ino_to_container.insert(ino, Container::new()); + self.tree + .ino_to_container + .get_mut(&Inode(parent)) + .unwrap() + .register(name.to_os_string(), ino); + // unwrap: we just created this inode. + let inode = self.lookup_inode(ino).unwrap(); + let file_attr = inode.mk_file_attrs(ino); + reply.entry(&DEFAULT_TTL, &file_attr, inode.generation); + } + + fn readdir( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + fh: u64, + offset: i64, + mut reply: fuser::ReplyDirectory, + ) { + let _ = fh; + let Some(inode_data) = self.lookup_inode_mut(Inode(ino)) else { + reply.error(libc::ENOENT); + return; + }; + if !inode_data.is_dir() { + reply.error(libc::ENOTDIR); + return; + } + let parent = inode_data.parent(); + let container = self + .tree + .ino_to_container + .get(&Inode(ino)) + .unwrap_or_else(|| panic!("ino {ino} is not in tree")); + let offset = u64::try_from(offset).unwrap(); + let standard = &[(DOT.as_os_str(), Inode(ino)), (DOTDOT.as_os_str(), parent)]; + let mut iter = standard + .iter() + .copied() + .chain(container.iter()) + .enumerate() + .skip(offset as usize); + loop { + if let Some((offset, (name, ino))) = iter.next() { + let inode = self + .lookup_inode(ino) + .unwrap_or_else(|| panic!("{ino:?} cannot be found")); + let offset = offset + 1; + let buf_is_filled = + reply.add(ino.0, offset.try_into().unwrap(), inode.kind(), name); + if buf_is_filled { + break; + } + } else { + break; + } + } + reply.ok(); + } + + fn rmdir( + &mut self, + _req: &fuser::Request<'_>, + parent: u64, + name: &OsStr, + reply: fuser::ReplyEmpty, + ) { + let Some(container) = self.tree.ino_to_container.get(&Inode(parent)) else { + reply.error(libc::ENOENT); + return; + }; + let Some(inode) = container.lookup_by_name(name) else { + reply.error(libc::ENOENT); + return; + }; + let Some(inode_data) = self.lookup_inode(inode) else { + reply.error(libc::ENOENT); + return; + }; + if !inode_data.is_dir() { + reply.error(libc::ENOTDIR); + return; + } + // unwrap: we checked that the dir exists above. + let container = self.tree.ino_to_container.get_mut(&Inode(parent)).unwrap(); + let Some(removed_ino) = container.remove(name) else { + reply.error(libc::ENOENT); + return; + }; + // Remove the tree entry corresponding to the removed inode. + let container = self + .tree + .ino_to_container + .remove(&removed_ino) + .unwrap_or_else(|| panic!("container was not present")); + for (_, item_ino) in container.iter() { + match self.lookup_inode(item_ino) { + None => { + panic!("container entry is not registered") + } + Some(inode_data) if inode_data.is_dir() => { + panic!("unexpected nested directory"); + } + Some(_) => { + self.remove_inode(item_ino); + } + } + } + self.remove_inode(removed_ino); + reply.ok(); + } + + fn unlink( + &mut self, + _req: &fuser::Request<'_>, + parent: u64, + name: &OsStr, + reply: fuser::ReplyEmpty, + ) { + let Some(inode_data) = self.lookup_inode_mut(Inode(parent)) else { + reply.error(libc::ENOENT); + return; + }; + if !inode_data.is_dir() { + reply.error(libc::ENOTDIR); + return; + } + let Some(container) = self.tree.ino_to_container.get_mut(&Inode(parent)) else { + reply.error(libc::ENOENT); + return; + }; + let Some(removed_ino) = container.remove(name) else { + reply.error(libc::ENOENT); + return; + }; + self.remove_inode(removed_ino); + reply.ok(); + } + + fn fallocate( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + fh: u64, + offset: i64, + length: i64, + mode: i32, + reply: fuser::ReplyEmpty, + ) { + let _ = (fh, mode); + let Some(inode_data) = self.lookup_inode_mut(Inode(ino)) else { + reply.error(libc::ENOENT); + return; + }; + // fallocate should preallocate stuff. We here just gon pretend that we are preallocating. + // we should set the length though. + let new_size = u64::try_from(offset).unwrap() + u64::try_from(length).unwrap(); + if inode_data.size() < new_size { + inode_data.set_size(new_size); + } + reply.ok(); + } + + fn fsync( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + fh: u64, + datasync: bool, + reply: fuser::ReplyEmpty, + ) { + // fsync doesn't do anything since we are working in-memory, so just return OK. + let _ = (ino, fh, datasync); + reply.ok(); + } + + fn fsyncdir( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: u64, + datasync: bool, + reply: fuser::ReplyEmpty, + ) { + // just like fsync, fsyncdir doesn't do anything. + let _ = (req, ino, fh, datasync); + reply.ok(); + } +} + +fn has_odirect(flags: i32) -> bool { + (flags & libc::O_DIRECT) != 0 +} + +pub struct TrickHandle { + bg_sess: fuser::BackgroundSession, +} + +impl TrickHandle { + /// Sets whether the file system should return ENOSPC on the subsequent write operations. + pub fn unmount_and_join(self) { + self.bg_sess.join(); + } +} + +/// A convenience function to spawn the trick file system. +/// +/// This allows directly depending on the libfuse API. +pub fn spawn_trick>(mountpoint: P) -> std::io::Result { + use fuser::MountOption; + let options = &[ + MountOption::RW, + MountOption::AutoUnmount, + MountOption::FSName("trick".to_string()), + ]; + let fs = Trick::new(); + let bg_sess = fuser::spawn_mount2(fs, &mountpoint, options)?; + Ok(TrickHandle { bg_sess }) +} + +#[cfg(test)] +mod tests { + use super::Trick; + use fuser::MountOption; + use std::{ + fs, + io::{Read, Seek, Write as _}, + os::fd::AsRawFd, + }; + + fn init_log() { + let _ = env_logger::builder() + .filter_level(log::LevelFilter::Trace) + .is_test(true) + .try_init(); + } + + // Just a smoke test to make sure the file system can be mounted and unmounted. + // + // If this fails then something is terribly wrong. + #[test] + fn mount() { + init_log(); + let mountpoint = tempfile::tempdir().unwrap(); + let options = &[MountOption::RO, MountOption::FSName("trick".to_string())]; + let fs = Trick::new(); + let mount_handle = fuser::spawn_mount2(fs, &mountpoint, options).unwrap(); + drop(mount_handle); + } + + // Create a file to the file system. + #[test] + fn create_file() { + init_log(); + let mountpoint = tempfile::tempdir().unwrap(); + let options = &[MountOption::RW, MountOption::FSName("trick".to_string())]; + let fs = Trick::new(); + let mount_handle = fuser::spawn_mount2(fs, &mountpoint, options).unwrap(); + let filename = mountpoint.path().join("file"); + let file = fs::File::create(&filename).unwrap(); + drop(file); + drop(mount_handle); + } + + #[test] + fn create_then_open_file() { + init_log(); + let mountpoint = tempfile::tempdir().unwrap(); + let options = &[MountOption::RW, MountOption::FSName("trick".to_string())]; + let fs = Trick::new(); + let mount_handle = fuser::spawn_mount2(fs, &mountpoint, options).unwrap(); + let filename = mountpoint.path().join("file"); + let file = fs::File::create(&filename).unwrap(); + drop(file); + let file = fs::File::open(&filename).unwrap(); + drop(file); + drop(mount_handle); + } + + #[test] + fn write_then_read() { + init_log(); + let mountpoint = tempfile::tempdir().unwrap(); + let options = &[ + MountOption::RW, + MountOption::AutoUnmount, + MountOption::FSName("trick".to_string()), + ]; + let fs = Trick::new(); + let mount_handle = fuser::spawn_mount2(fs, &mountpoint, options).unwrap(); + let filename = mountpoint.path().join("file"); + let mut file = fs::File::options() + .create_new(true) + .write(true) + .open(&filename) + .unwrap(); + let test_data = b"hello world"; + file.write_all(test_data).unwrap(); + drop(file); + // reopen and read + let mut file = fs::File::open(&filename).unwrap(); + let mut buf = Vec::new(); + file.read_to_end(&mut buf).unwrap(); + assert_eq!(test_data, buf.as_slice()); + drop(file); + drop(mount_handle); + } + + // Create many files and write to them at increasing offsets. + // + // This is supposed to test that we can handle many sparse files and that it does not run out + // of memory. + #[test] + fn many_files() { + init_log(); + let mountpoint = tempfile::tempdir().unwrap(); + let options = &[ + MountOption::RW, + MountOption::AutoUnmount, + MountOption::FSName("trick".to_string()), + ]; + let fs = Trick::new(); + let mount_handle = fuser::spawn_mount2(fs, &mountpoint, options).unwrap(); + let mut files = Vec::new(); + for i in 0..100 { + let filename = mountpoint.path().join(format!("file{}", i)); + let mut file = fs::File::options() + .create_new(true) + .write(true) + .open(&filename) + .unwrap(); + let test_data = format!("hello world {}", i); + file.seek_relative(i * 10_000).unwrap(); + file.write_all(test_data.as_bytes()).unwrap(); + files.push(file); + } + for file in files { + drop(file); + } + drop(mount_handle); + } + + /// Create and remove a file. + #[test] + fn unlink() { + init_log(); + let mountpoint = tempfile::tempdir().unwrap(); + let options = &[ + MountOption::RW, + MountOption::AutoUnmount, + MountOption::FSName("trick".to_string()), + ]; + let fs = Trick::new(); + let mount_handle = fuser::spawn_mount2(fs, &mountpoint, options).unwrap(); + + // Create a file + let filename = mountpoint.path().join("file_to_unlink"); + let file = fs::File::create(&filename).unwrap(); + drop(file); + assert!(filename.exists()); + fs::remove_file(&filename).unwrap(); + assert!(!filename.exists()); + drop(mount_handle); + } + + #[test] + fn mmap_test() { + init_log(); + let mountpoint = tempfile::tempdir().unwrap(); + let options = &[ + MountOption::RW, + MountOption::AutoUnmount, + MountOption::FSName("trick".to_string()), + ]; + let fs = Trick::new(); + let mount_handle = fuser::spawn_mount2(fs, &mountpoint, options).unwrap(); + + let filename = mountpoint.path().join("file"); + let mut file = fs::File::options() + .create_new(true) + .write(true) + .open(&filename) + .unwrap(); + let test_data = b"hello world"; + file.write_all(test_data).unwrap(); + drop(file); + + let file = fs::File::open(&filename).unwrap(); + unsafe { + let data = libc::mmap( + std::ptr::null_mut(), + 4096, + libc::PROT_READ, + libc::MAP_PRIVATE, + file.as_raw_fd(), + 0, + ); + assert_ne!(data, libc::MAP_FAILED); + let data: *mut u8 = data.cast(); + let slice = std::slice::from_raw_parts(data, 4096); + assert_eq!(&test_data[..], &slice[0..test_data.len()][..]); + } + + drop(file); + drop(mount_handle); + } +} diff --git a/trickfs/src/main.rs b/trickfs/src/main.rs new file mode 100644 index 00000000..b23ff7e5 --- /dev/null +++ b/trickfs/src/main.rs @@ -0,0 +1,9 @@ +fn main() { + let _ = env_logger::builder() + .filter_level(log::LevelFilter::Trace) + .init(); + let handle = trickfs::spawn_trick("/tmp/trick").unwrap(); + println!("running..."); + std::io::stdin().read_line(&mut String::new()).unwrap(); + drop(handle); +}