//! Fuzzer that exercises random file system operations against a FUSE-mounted redoxfs. #![no_main] use anyhow::{ensure, Result}; use fuser; use libfuzzer_sys::{arbitrary::Arbitrary, fuzz_target, Corpus}; use nix::sys::statvfs::statvfs; use std::{ fs::{self, File, FileTimes, OpenOptions}, io::{Read, Seek, SeekFrom, Write}, os::unix::fs::{self as unix_fs, PermissionsExt}, path::{Path, PathBuf}, thread, time::{Duration, SystemTime, UNIX_EPOCH}, }; use tempfile; use redoxfs::{mount::fuse::Fuse, DiskSparse, FileSystem}; /// Maximum size for files and buffers. Chosen arbitrarily with fuzzing performance in mind. const MAX_SIZE: u64 = 10_000_000; /// Limit on the number of remounts in a single test case. Chosen arbitrarily with fuzzing /// performance in mind: remounts are costly. const MAX_MOUNT_SEQUENCES: usize = 3; /// An operation to be performed by the fuzzer. #[derive(Arbitrary, Clone, Debug)] enum Operation { Chown { path: PathBuf, uid: Option, gid: Option, }, CreateDir { path: PathBuf, }, HardLink { original: PathBuf, link: PathBuf, }, Metadata { path: PathBuf, }, Read { path: PathBuf, }, ReadDir { path: PathBuf, }, ReadLink { path: PathBuf, }, RemoveDir { path: PathBuf, }, RemoveFile { path: PathBuf, }, Rename { from: PathBuf, to: PathBuf, }, SeekRead { path: PathBuf, seek_pos: u64, buf_size: usize, }, SeekWrite { path: PathBuf, seek_pos: u64, buf_size: usize, }, SetLen { path: PathBuf, size: u64, }, SetPermissions { path: PathBuf, readonly: Option, mode: Option, }, SetTimes { path: PathBuf, accessed_since_epoch: Option, modified_since_epoch: Option, }, Statvfs {}, SymLink { original: PathBuf, link: PathBuf, }, Write { path: PathBuf, buf_size: usize, }, } /// Parameters for mounting the file system and operations to be performed afterwards. #[derive(Arbitrary, Clone, Debug)] struct MountSequence { squash: bool, operations: Vec, } /// The whole input to a single fuzzer invocation. #[derive(Arbitrary, Clone, Debug)] struct TestCase { disk_size: u64, reserved_size: u64, mount_sequences: Vec, } /// Creates the disk for backing the Redoxfs. fn create_disk(temp_path: &Path, disk_size: u64) -> DiskSparse { let disk_path = temp_path.join("disk.img"); DiskSparse::create(disk_path, disk_size).unwrap() } /// Creates an empty Redoxfs. fn create_redoxfs(disk: DiskSparse, reserved_size: u64) -> bool { let password = None; let reserved = vec![0; reserved_size as usize]; let ctime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); FileSystem::create_reserved( disk, password, &reserved, ctime.as_secs(), ctime.subsec_nanos(), ) .is_ok() } /// Mounts an existing Redoxfs, runs the callback and performs the unmount. fn with_redoxfs_mount(temp_path: &Path, disk: DiskSparse, squash: bool, callback: F) where F: FnOnce(&Path) + Send + 'static, { let password = None; let block = None; let mut fs = FileSystem::open(disk, password, block, squash).unwrap(); let mount_path = temp_path.join("mount"); fs::create_dir_all(&mount_path).unwrap(); let mut session = fuser::Session::new(Fuse { fs: &mut fs }, &mount_path, &[]).unwrap(); let mut unmounter = session.unmount_callable(); let join_handle = thread::spawn(move || { callback(&mount_path); unmounter.unmount().unwrap(); }); session.run().unwrap(); join_handle.join().unwrap(); } fn get_path_within_fs(fs_path: &Path, path_to_add: &Path) -> Result { ensure!(path_to_add.is_relative()); ensure!(path_to_add .components() .all(|c| c != std::path::Component::ParentDir)); Ok(fs_path.join(path_to_add)) } fn do_operation(fs_path: &Path, op: &Operation) -> Result<()> { match op { Operation::Chown { path, uid, gid } => { let path = get_path_within_fs(fs_path, path)?; unix_fs::chown(path, *uid, *gid)?; } Operation::CreateDir { path } => { let path = get_path_within_fs(fs_path, path)?; fs::create_dir(path)?; } Operation::HardLink { original, link } => { let original = get_path_within_fs(fs_path, original)?; let link = get_path_within_fs(fs_path, link)?; fs::hard_link(original, link)?; } Operation::Metadata { path } => { let path = get_path_within_fs(fs_path, path)?; fs::metadata(path)?; } Operation::Read { path } => { let path = get_path_within_fs(fs_path, path)?; fs::read(path)?; } Operation::ReadDir { path } => { let path = get_path_within_fs(fs_path, path)?; let _ = fs::read_dir(path)?.count(); } Operation::ReadLink { path } => { let path = get_path_within_fs(fs_path, path)?; fs::read_link(path)?; } Operation::RemoveDir { path } => { let path = get_path_within_fs(fs_path, path)?; fs::remove_dir(path)?; } Operation::RemoveFile { path } => { let path = get_path_within_fs(fs_path, path)?; fs::remove_file(path)?; } Operation::Rename { from, to } => { let from = get_path_within_fs(fs_path, from)?; let to = get_path_within_fs(fs_path, to)?; fs::rename(from, to)?; } Operation::SeekRead { path, seek_pos, buf_size, } => { ensure!(*buf_size as u64 <= MAX_SIZE); let path = get_path_within_fs(fs_path, path)?; let mut file = File::open(path)?; file.seek(SeekFrom::Start(*seek_pos))?; let mut buf = vec![0; *buf_size]; file.read(&mut buf)?; } Operation::SeekWrite { path, seek_pos, buf_size, } => { ensure!(*seek_pos <= MAX_SIZE); ensure!(*buf_size as u64 <= MAX_SIZE); let path = get_path_within_fs(fs_path, path)?; let mut file = OpenOptions::new().write(true).open(path)?; file.seek(SeekFrom::Start(*seek_pos))?; let buf = vec![0; *buf_size]; file.write(&buf)?; } Operation::SetLen { path, size } => { let path = get_path_within_fs(fs_path, path)?; let file = OpenOptions::new().write(true).open(path)?; file.set_len(*size)?; } Operation::SetPermissions { path, readonly, mode, } => { let path = get_path_within_fs(fs_path, path)?; let metadata = fs::metadata(&path)?; let mut perms = metadata.permissions(); if let Some(readonly) = readonly { perms.set_readonly(*readonly); } if let Some(mode) = mode { perms.set_mode(*mode); } fs::set_permissions(path, perms)?; } Operation::SetTimes { path, accessed_since_epoch, modified_since_epoch, } => { let path = get_path_within_fs(fs_path, path)?; let file = File::options().write(true).open(path)?; let mut times = FileTimes::new(); if let Some(accessed_since_epoch) = accessed_since_epoch { if let Some(accessed) = UNIX_EPOCH.checked_add(*accessed_since_epoch) { times = times.set_accessed(accessed); } } if let Some(modified_since_epoch) = modified_since_epoch { if let Some(modified) = UNIX_EPOCH.checked_add(*modified_since_epoch) { times = times.set_modified(modified); } } file.set_times(times)?; } Operation::Statvfs {} => { statvfs(fs_path)?; } Operation::SymLink { original, link } => { let original = get_path_within_fs(fs_path, original)?; let link = get_path_within_fs(fs_path, link)?; unix_fs::symlink(original, link)?; } Operation::Write { path, buf_size } => { ensure!(*buf_size as u64 <= MAX_SIZE); let path = get_path_within_fs(fs_path, path)?; let buf = vec![0; *buf_size]; fs::write(path, &buf)?; } } Ok(()) } fuzz_target!(|test_case: TestCase| -> Corpus { if test_case.disk_size > MAX_SIZE || test_case.reserved_size > MAX_SIZE || test_case.mount_sequences.len() > MAX_MOUNT_SEQUENCES { return Corpus::Reject; } let temp_dir = tempfile::Builder::new() .prefix("fuse_fuzz_target") .tempdir() .unwrap(); #[cfg(feature = "log")] eprintln!("create fs"); let disk = create_disk(temp_dir.path(), test_case.disk_size); if !create_redoxfs(disk, test_case.reserved_size) { // File system creation failed (e.g., due to insufficient space) so we bail out, still // exercising this code path is useful. return Corpus::Keep; } for mount_seq in test_case.mount_sequences.iter() { #[cfg(feature = "log")] eprintln!("mount fs"); let disk = create_disk(temp_dir.path(), test_case.disk_size); let operations = mount_seq.operations.clone(); with_redoxfs_mount(temp_dir.path(), disk, mount_seq.squash, move |fs_path| { for operation in operations.iter() { #[cfg(feature = "log")] eprintln!("do operation {operation:?}"); let _result = do_operation(fs_path, operation); #[cfg(feature = "log")] eprintln!("operation result {:?}", _result.err()); } }); #[cfg(feature = "log")] eprintln!("unmounted fs"); } Corpus::Keep });