Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split into library and binary parts and implement fuzzer #74

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
def288f
move src/ufs.rs -> src/ufs/mod.rs
realchonk Sep 5, 2024
c228f0e
Ufs: factor out xattr code into xattr.rs
realchonk Sep 5, 2024
576f09c
factor out symlink code into symlink.rs
realchonk Sep 5, 2024
0712592
move inode-related stuff into inode.rs
realchonk Sep 5, 2024
5012115
move dir-related stuff into dir.rs
realchonk Sep 5, 2024
df797a1
refactor Ufs::read() into Ufs::inode_read()
realchonk Sep 5, 2024
e242a43
move bin part into src/bin/fuser/
realchonk Sep 5, 2024
d084287
refactor Ufs::init() into Ufs::check()
realchonk Sep 5, 2024
1b4a4bf
refactor Ufs::statfs() into Ufs::info()
realchonk Sep 5, 2024
d640c0b
export Info
realchonk Sep 5, 2024
86d08ba
export Ufs::read_inode() and Inode
realchonk Sep 5, 2024
440cd8b
move fuser-specific parts into fuser bin
realchonk Sep 5, 2024
15e5098
fmt
realchonk Sep 5, 2024
473af8d
fix test suite
realchonk Sep 5, 2024
35e9a61
lint
realchonk Sep 5, 2024
31b953c
Split binary and library out into separate crates
casept Sep 11, 2024
fcbd70f
use opaque InodeNum instead of u32/u64 directly
realchonk Sep 11, 2024
d9622de
safety is #1 priority
realchonk Sep 11, 2024
3a51b80
Implement rudimentary fuzzer
casept Sep 11, 2024
1eb1d08
rufs: remove anyhow dependency
realchonk Sep 11, 2024
8014fbe
de-fuser-ify rufs
realchonk Sep 11, 2024
da0f734
rufs: make fuser optional
realchonk Sep 11, 2024
db985ae
fmt
realchonk Sep 11, 2024
37ea963
lint
realchonk Sep 11, 2024
6a4af05
add casept as author
realchonk Sep 11, 2024
da32530
remove binary
realchonk Sep 16, 2024
8aa2577
Ufs::new(): run checks, but ignore result when fuzzing
realchonk Sep 16, 2024
d3eba9e
Ufs::check(): add more checks
realchonk Sep 20, 2024
d687be7
fmt
realchonk Sep 20, 2024
4522016
Ufs::new(): also run check when fuzzing
realchonk Sep 20, 2024
0b9be55
Ufs::check(): fix checks
realchonk Sep 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ clean:
cargo clean

fuse-ufs-bin: Cargo.lock ${SRC}
cargo build --release
cargo build --release -p fuse-ufs
cp -f target/release/fuse-ufs fuse-ufs-bin

4 changes: 2 additions & 2 deletions fuse-ufs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "fuse-ufs"
version = "0.3.0"
edition = "2021"
license = "BSD-2-Clause"
authors = ["Benjamin Stürz <benni@stuerz.xyz>", "Alan Somers <asomers@gmail.com>"]
authors = ["Benjamin Stürz <benni@stuerz.xyz>", "Alan Somers <asomers@gmail.com>", "Davids Paskevics <davids.paskevics@gmail.com>"]
description = "FUSE implementation of FreeBSD's UFSv2"
repository = "https://github.com/realchonk/fuse-ufs"
rust-version = "1.74.0"
Expand All @@ -19,7 +19,7 @@ env_logger.workspace = true
fuser.workspace = true
libc.workspace = true
log.workspace = true
rufs = { path = "../rufs" }
rufs = { path = "../rufs", features = ["fuser"] }

[dev-dependencies]
assert_cmd.workspace = true
Expand Down
14 changes: 7 additions & 7 deletions fuse-ufs/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
time::Duration,
};

use fuser::{Filesystem, KernelConfig, Request};
use fuser::{FileAttr, Filesystem, KernelConfig, Request};
use rufs::{InodeNum, Ufs};

const MAX_CACHE: Duration = Duration::MAX;
Expand Down Expand Up @@ -52,8 +52,8 @@ impl Filesystem for Fs {
// TODO: don't use read_inode()
let f = || {
let inr = transino(ino)?;
let ino = self.ufs.read_inode(inr)?;
Ok(ino.as_fileattr(inr))
let st: FileAttr = self.ufs.inode_attr(inr)?.into();
Ok(st)
};
match run(f) {
Ok(x) => reply.attr(&MAX_CACHE, &x),
Expand Down Expand Up @@ -90,7 +90,7 @@ impl Filesystem for Fs {

self.ufs.dir_iter(inr, |name, inr, kind| {
i += 1;
if i > offset && reply.add(inr.get64(), i, kind, name) {
if i > offset && reply.add(inr.get64(), i, kind.into(), name) {
return Some(());
}
None
Expand All @@ -108,12 +108,12 @@ impl Filesystem for Fs {
let mut f = || {
let pinr = transino(pinr)?;
let inr = self.ufs.dir_lookup(pinr, name)?;
let ino = self.ufs.read_inode(inr)?;
Ok::<_, IoError>((ino.as_fileattr(inr), ino.gen))
let st = self.ufs.inode_attr(inr)?;
Ok::<_, IoError>((st.gen, st.into()))
};

match f() {
Ok((attr, gen)) => reply.entry(&Duration::ZERO, &attr, gen.into()),
Ok((gen, st)) => reply.entry(&Duration::ZERO, &st, gen.into()),
Err(e) => {
if e.kind() != ErrorKind::NotFound {
log::error!("Error: {e}");
Expand Down
7 changes: 5 additions & 2 deletions rufs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ name = "rufs"
version = "0.3.0"
edition = "2021"
license = "BSD-2-Clause"
authors = ["Benjamin Stürz <benni@stuerz.xyz>", "Alan Somers <asomers@gmail.com>"]
authors = ["Benjamin Stürz <benni@stuerz.xyz>", "Alan Somers <asomers@gmail.com>", "Davids Paskevics <davids.paskevics@gmail.com>"]
description = "FUSE implementation of FreeBSD's UFSv2"
repository = "https://github.com/realchonk/fuse-ufs"
rust-version = "1.74.0"
documentation = "https://docs.rs/rufs"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[features]
fuser = ["dep:fuser"]
asomers marked this conversation as resolved.
Show resolved Hide resolved

realchonk marked this conversation as resolved.
Show resolved Hide resolved
[dependencies]
bincode.workspace = true
fuser.workspace = true
fuser = { workspace = true, optional = true }
libc.workspace = true
log.workspace = true

Expand Down
34 changes: 34 additions & 0 deletions rufs/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::{
ffi::{OsStr, OsString},
fmt::{self, Display, Formatter},
mem::size_of,
time::SystemTime,
};

use bincode::Decode;
Expand Down Expand Up @@ -356,6 +357,39 @@ pub struct Inode {
pub spare: [u32; 2], // 248: Reserved; currently unused
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InodeType {
RegularFile,
Directory,
Symlink,
CharDevice,
BlockDevice,
Socket,
NamedPipe,
//Whiteout,
}

#[derive(Debug)]
pub struct InodeAttr {
pub inr: InodeNum,
pub perm: u16,
pub kind: InodeType,
pub size: u64,
pub blocks: u64,
pub atime: SystemTime,
pub mtime: SystemTime,
pub ctime: SystemTime,
pub btime: SystemTime,
pub nlink: u16,
pub uid: u32,
pub gid: u32,
pub gen: u32,
pub blksize: u32,
pub flags: u32,
pub kernflags: u32,
pub extsize: u32,
}

#[derive(Debug, Clone, Copy, Decode, PartialEq, Eq)]
#[repr(u8)]
pub enum ExtattrNamespace {
Expand Down
98 changes: 71 additions & 27 deletions rufs/src/inode.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::time::{Duration, SystemTime};

use bincode::{de::Decoder, error::DecodeError, Decode};
use fuser::{FileAttr, FileType};

use crate::data::*;

Expand Down Expand Up @@ -41,44 +40,46 @@ impl Inode {
self.mode & 0o7777
}

pub fn kind(&self) -> FileType {
pub fn kind(&self) -> InodeType {
let mode = self.mode & S_IFMT;
match mode {
S_IFIFO => FileType::NamedPipe,
S_IFCHR => FileType::CharDevice,
S_IFDIR => FileType::Directory,
S_IFBLK => FileType::BlockDevice,
S_IFREG => FileType::RegularFile,
S_IFLNK => FileType::Symlink,
S_IFSOCK => FileType::Socket,
S_IFIFO => InodeType::NamedPipe,
S_IFCHR => InodeType::CharDevice,
S_IFDIR => InodeType::Directory,
S_IFBLK => InodeType::BlockDevice,
S_IFREG => InodeType::RegularFile,
S_IFLNK => InodeType::Symlink,
S_IFSOCK => InodeType::Socket,
_ => unreachable!("invalid file mode: {mode:o}"),
}
}

pub fn as_fileattr(&self, ino: InodeNum) -> FileAttr {
FileAttr {
ino: ino.get64(),
size: self.size,
blocks: self.blocks,
atime: self.atime(),
mtime: self.mtime(),
ctime: self.ctime(),
crtime: self.btime(),
kind: self.kind(),
perm: self.perm(),
nlink: self.nlink.into(),
uid: self.uid,
gid: self.gid,
rdev: 0,
pub fn as_attr(&self, inr: InodeNum) -> InodeAttr {
InodeAttr {
inr,
perm: self.mode & 0o7777,
kind: self.kind(),
size: self.size,
blocks: self.blocks,
atime: self.atime(),
mtime: self.mtime(),
ctime: self.ctime(),
btime: self.btime(),
nlink: self.nlink,
uid: self.uid,
gid: self.gid,
gen: self.gen,
blksize: self.blksize,
flags: self.flags,
flags: self.flags,
kernflags: self.kernflags,
extsize: self.extsize,
}
}

pub fn size(&self, bs: u64, fs: u64) -> (u64, u64) {
let size = match self.kind() {
FileType::Directory => self.blocks * fs,
FileType::RegularFile | FileType::Symlink => self.size,
InodeType::Directory => self.blocks * fs,
InodeType::RegularFile | InodeType::Symlink => self.size,
kind => todo!("Inode::size() is undefined for {kind:?}"),
};
Self::inode_size(bs, fs, size)
Expand Down Expand Up @@ -169,3 +170,46 @@ mod test {
assert_eq!(isz(100 * bs + 7 * fs), (100, 7));
}
}

#[cfg(feature = "fuser")]
mod f {
use fuser::{FileAttr, FileType};

use super::*;

impl From<InodeType> for FileType {
fn from(t: InodeType) -> Self {
match t {
InodeType::RegularFile => Self::RegularFile,
InodeType::Directory => Self::Directory,
InodeType::Symlink => Self::Symlink,
InodeType::Socket => Self::Socket,
InodeType::CharDevice => Self::CharDevice,
InodeType::BlockDevice => Self::BlockDevice,
InodeType::NamedPipe => Self::NamedPipe,
}
}
}

impl From<InodeAttr> for FileAttr {
fn from(a: InodeAttr) -> Self {
Self {
ino: a.inr.get64(),
size: a.size,
blocks: a.blocks,
atime: a.atime,
mtime: a.mtime,
ctime: a.ctime,
crtime: a.atime,
kind: a.kind.into(),
perm: a.perm,
nlink: a.nlink.into(),
uid: a.uid,
gid: a.gid,
rdev: 0,
blksize: a.blksize,
flags: a.flags,
}
}
}
}
18 changes: 9 additions & 9 deletions rufs/src/ufs/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ fn readdir_block<T>(
inr: InodeNum,
block: &[u8],
config: Config,
mut f: impl FnMut(&OsStr, InodeNum, FileType) -> Option<T>,
mut f: impl FnMut(&OsStr, InodeNum, InodeType) -> Option<T>,
) -> IoResult<Option<T>> {
let mut name = [0u8; UFS_MAXNAMELEN + 1];
let file = Cursor::new(block);
Expand All @@ -31,13 +31,13 @@ fn readdir_block<T>(

let name = unsafe { OsStr::from_encoded_bytes_unchecked(name) };
let kind = match kind {
DT_FIFO => FileType::NamedPipe,
DT_CHR => FileType::CharDevice,
DT_DIR => FileType::Directory,
DT_BLK => FileType::BlockDevice,
DT_REG => FileType::RegularFile,
DT_LNK => FileType::Symlink,
DT_SOCK => FileType::Socket,
DT_FIFO => InodeType::NamedPipe,
DT_CHR => InodeType::CharDevice,
DT_DIR => InodeType::Directory,
DT_BLK => InodeType::BlockDevice,
DT_REG => InodeType::RegularFile,
DT_LNK => InodeType::Symlink,
DT_SOCK => InodeType::Socket,
DT_WHT => {
log::warn!("readdir_block({inr}): encountered a whiteout entry: {name:?}");
continue;
Expand Down Expand Up @@ -72,7 +72,7 @@ impl<R: Read + Seek> Ufs<R> {
pub fn dir_iter<T>(
&mut self,
inr: InodeNum,
mut f: impl FnMut(&OsStr, InodeNum, FileType) -> Option<T>,
mut f: impl FnMut(&OsStr, InodeNum, InodeType) -> Option<T>,
) -> IoResult<Option<T>> {
let ino = self.read_inode(inr)?;
let mut block = vec![0u8; self.superblock.bsize as usize];
Expand Down
7 changes: 6 additions & 1 deletion rufs/src/ufs/inode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ use super::*;
use crate::{err, InodeNum};

impl<R: Read + Seek> Ufs<R> {
pub fn inode_attr(&mut self, inr: InodeNum) -> IoResult<InodeAttr> {
let ino = self.read_inode(inr)?;
Ok(ino.as_attr(inr))
}

pub fn inode_read(
&mut self,
inr: InodeNum,
Expand Down Expand Up @@ -34,7 +39,7 @@ impl<R: Read + Seek> Ufs<R> {
Ok(boff)
}

pub fn read_inode(&mut self, inr: InodeNum) -> IoResult<Inode> {
pub(super) fn read_inode(&mut self, inr: InodeNum) -> IoResult<Inode> {
let off = self.superblock.ino_to_fso(inr);
let ino: Inode = self.file.decode_at(off)?;

Expand Down
15 changes: 11 additions & 4 deletions rufs/src/ufs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ use std::{
path::Path,
};

use fuser::FileType;

mod dir;
mod inode;
mod symlink;
Expand Down Expand Up @@ -68,7 +66,12 @@ impl<R: Read + Seek> Ufs<R> {
let config = match magic {
[0x19, 0x01, 0x54, 0x19] => Config::little(),
[0x19, 0x54, 0x01, 0x19] => Config::big(),
_ => iobail!(ErrorKind::InvalidInput, "invalid superblock magic number: {magic:?}"),
_ => {
iobail!(
ErrorKind::InvalidInput,
"invalid superblock magic number: {magic:?}"
)
}
};
// FIXME: Choose based on hash of input or so, to excercise BE as well with introducing non-determinism
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that when fuzzing, you should guess the endianess just like you do when not fuzzing. You can generate multiple corpuses, right? One based on BE and one based on LE?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as with the superblock check below - wasted fuzz cases due to guessing an invalid magic.

#[cfg(fuzzing)]
Expand All @@ -79,7 +82,11 @@ impl<R: Read + Seek> Ufs<R> {
let superblock: Superblock = file.decode_at(SBLOCK_UFS2 as u64)?;
#[cfg(not(fuzzing))]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why disable this check when fuzzing?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the fuzzer doesn't have to guess a valid superblock, which would lead to a lot of wasted fuzz cases as they'd fail at this stage without reaching crashes/panics deeper in the code. It's common practice to disable these sorts of checks while fuzzing for this reason.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fuzzer doesn't generate totally random input, right? It starts from a valid disk image and then randomly mutates it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

if superblock.magic != FS_UFS2_MAGIC {
iobail!(ErrorKind::InvalidInput, "invalid superblock magic number: {}", superblock.magic);
iobail!(
ErrorKind::InvalidInput,
"invalid superblock magic number: {}",
superblock.magic
);
}
let mut s = Self { file, superblock };
#[cfg(not(fuzzing))]
Expand Down
2 changes: 1 addition & 1 deletion rufs/src/ufs/symlink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ impl<R: Read + Seek> Ufs<R> {
pub fn symlink_read(&mut self, inr: InodeNum) -> IoResult<Vec<u8>> {
let ino = self.read_inode(inr)?;

if ino.kind() != FileType::Symlink {
if ino.mode & S_IFMT != S_IFLNK {
return Err(IoError::from_raw_os_error(libc::EINVAL));
}

Expand Down