686 lines
19 KiB
Rust
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,
|
|
})
|
|
}
|