686 lines
19 KiB
Rust

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<u8>,
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<u64, File>, // inode -> File
inode_map: HashMap<OsString, u64>, // name -> inode
open_fds: HashMap<u64, OpenFd>, // 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<u32>,
uid: Option<u32>,
gid: Option<u32>,
size: Option<u64>,
atime: Option<fuser::TimeOrNow>,
mtime: Option<fuser::TimeOrNow>,
ctime: Option<SystemTime>,
fh: Option<u64>,
crtime: Option<SystemTime>,
_chgtime: Option<SystemTime>,
_bkuptime: Option<SystemTime>,
_flags: Option<u32>,
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<u64>,
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::<Vec<u8>>());
//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<u64>,
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<u64>,
_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<u32>,
uid: Option<u32>,
gid: Option<u32>,
size: Option<u64>,
atime: Option<TimeOrNow>,
mtime: Option<TimeOrNow>,
ctime: Option<SystemTime>,
crtime: Option<SystemTime>,
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<TimeOrNow>) -> Option<SystemTime> {
t.map(|t| match t {
TimeOrNow::Now => SystemTime::now(),
TimeOrNow::SpecificTime(st) => st,
})
}