commit 896545333b25a4a726993b00c3e398eed70dde29 Author: Brian J. Tarricone Date: Wed Oct 23 02:55:27 2024 -0700 Initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2a27cc0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,499 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "corruptfs-fuse" +version = "0.1.0" +dependencies = [ + "clap", + "env_logger", + "fuser", + "log", + "nix", + "rand", +] + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "fuser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e697f6f62c20b6fad1ba0f84ae909f25971cf16e735273524e3977c94604cf8" +dependencies = [ + "libc", + "log", + "memchr", + "page_size", + "pkg-config", + "smallvec", + "zerocopy", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[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 = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d834b61 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "corruptfs-fuse" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.20", features = ["derive"] } +env_logger = "0.11.5" +fuser = { version = "0.14.0", features = ["abi-7-30"] } +log = { version = "0.4.22", features = ["std"] } +nix = { version = "0.29.0", features = ["user"] } +rand = "0.8.5" diff --git a/README.md b/README.md new file mode 100644 index 0000000..a0a3bf2 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +This is a FUSE filesystem that always returns random corrupted data when +you read back files you write. + +The filesystem will tell the kernel to use its filesystem buffer cache, +so you won't see the corruption unless you read with `O_DIRECT`, or if +you tell the kernel to drop its caches. + +Run with: + +``` +mkdir ~/corrupt-fs +cargo run -- --allow-direct-io ~/corrupt-fs +``` + +If you don't specify `--allow-direct-io`, direct I/O will not be +supported. + +To test from the cmdline: + +``` +echo "hello world" > ~/corrupt-fs/hello.txt +cat ~/corrupt-fs/hello.txt # returns good data from fscache +dd if=~/corrupt-fs/hello.txt iflag=direct # returns bad data +``` + +Limitations: + +* Subdirectories are not supported. +* `lseek()` modes other than `SEEK_SET` are not supported. +* There are probably bugs, especially with large files. diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 0000000..1025e76 --- /dev/null +++ b/src/fs.rs @@ -0,0 +1,700 @@ +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 mut o_flags = FOPEN_KEEP_CACHE; + if (flags & O_DIRECT) != 0 { + o_flags |= FOPEN_DIRECT_IO; + } + + 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 mut cr_flags = FOPEN_KEEP_CACHE; + if (flags & O_DIRECT) != 0 { + cr_flags |= FOPEN_DIRECT_IO; + } + + 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, + }) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c6e9181 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,50 @@ +#[macro_use] +extern crate log; + +use clap::Parser; +use env_logger::Env; +use fuser::MountOption; + +mod fs; + +#[derive(Parser)] +#[command(version, about, long_about = None)] +struct Args { + /// Pass FUSE options to library + #[arg(short = 'o', value_name = "OPT")] + options: Vec, + + /// Allow opening files with O_DIRECT + #[arg(long = "allow-direct-io")] + allow_direct_io: bool, + + /// Mount point + #[arg(value_name = "MOUNT_POINT")] + mount_point: String, +} + +fn main() { + env_logger::init_from_env(Env::new().filter("CORRUPTFS_LOG")); + + let args = Args::parse(); + debug!("extra options: {:?}", args.options); + + let mut options = vec![ + MountOption::FSName("corruptfs".to_string()), + MountOption::NoDev, + MountOption::NoSuid, + MountOption::RW, + ]; + options.extend( + args.options + .iter() + .map(|opt| MountOption::CUSTOM(opt.clone())), + ); + + fuser::mount2( + fs::CorruptFs::new(args.allow_direct_io), + args.mount_point, + &options, + ) + .unwrap(); +}