use fs_ext4::block_io::FileDevice;
use fs_ext4::Filesystem;
use std::fs;
use std::io::{Seek, SeekFrom, Write};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
fn image_path(name: &str) -> String {
format!("{}/test-disks/{}", env!("CARGO_MANIFEST_DIR"), name)
}
fn copy_to_tmp(name: &str, tag: &str) -> Option<String> {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let src = image_path(name);
if !std::path::Path::new(&src).exists() {
return None;
}
let dst = format!("/tmp/fs_ext4_fuzzwp_{}_{tag}_{n}.img", std::process::id());
fs::copy(&src, &dst).ok()?;
Some(dst)
}
fn stomp(path: &str, offset: u64, len: usize) {
let mut f = fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.expect("open");
f.seek(SeekFrom::Start(offset)).expect("seek");
let pattern: Vec<u8> = (0..len).map(|i| (i ^ 0xC3) as u8).collect();
f.write_all(&pattern).expect("stomp");
}
fn try_call<F: FnOnce() + std::panic::UnwindSafe>(f: F) -> std::thread::Result<()> {
std::panic::catch_unwind(f)
}
#[test]
fn write_paths_on_stomped_inode_table_never_panic() {
let Some(path) = copy_to_tmp("ext4-basic.img", "inode_stomp") else {
return;
};
stomp(&path, 24576, 4096);
let result = try_call(|| {
let dev = FileDevice::open_rw(&path).expect("open");
let Ok(fs) = Filesystem::mount(Arc::new(dev)) else {
return;
};
let _ = fs.apply_chmod("/test.txt", 0o644);
let _ = fs.apply_chown("/test.txt", 1, 1);
let _ = fs.apply_utimens("/test.txt", 1_700_000_000, 0, 1_700_000_000, 0);
let _ = fs.apply_setxattr("/test.txt", "user.x", b"v");
let _ = fs.apply_removexattr("/test.txt", "user.x");
let _ = fs.apply_unlink("/test.txt");
});
assert!(result.is_ok(), "write op panicked on stomped inode table");
fs::remove_file(path).ok();
}
#[test]
fn write_paths_on_stomped_block_bitmap_never_panic() {
let Some(path) = copy_to_tmp("ext4-basic.img", "bitmap_stomp") else {
return;
};
stomp(&path, 8192, 4096);
let result = try_call(|| {
let dev = FileDevice::open_rw(&path).expect("open");
let Ok(fs) = Filesystem::mount(Arc::new(dev)) else {
return;
};
let mut reader = |ino: u32| fs.read_inode_verified(ino).map(|(i, _)| i);
if let Ok(ino) = fs_ext4::path::lookup(fs.dev.as_ref(), &fs.sb, &mut reader, "/test.txt") {
let _ = fs.apply_truncate_shrink(ino, 0);
}
let _ = fs.apply_unlink("/test.txt");
});
assert!(result.is_ok(), "write op panicked on stomped bitmap");
fs::remove_file(path).ok();
}
#[test]
fn extreme_setxattr_value_lengths_never_panic() {
let Some(path) = copy_to_tmp("ext4-basic.img", "extreme_xattr") else {
return;
};
let result = try_call(|| {
let dev = FileDevice::open_rw(&path).expect("open");
let fs = Filesystem::mount(Arc::new(dev)).expect("mount");
let _ = fs.apply_setxattr("/test.txt", "user.empty", b"");
let big = vec![0xCDu8; 1024 * 1024];
let _ = fs.apply_setxattr("/test.txt", "user.giant", &big);
let _ = fs.apply_setxattr("/test.txt", "weird.nope", b"v");
let _ = fs.apply_setxattr("/test.txt", "user.", b"v");
});
assert!(result.is_ok(), "setxattr edge cases panicked");
fs::remove_file(path).ok();
}
#[test]
fn extreme_truncate_sizes_never_panic() {
let Some(path) = copy_to_tmp("ext4-basic.img", "extreme_trunc") else {
return;
};
let result = try_call(|| {
let dev = FileDevice::open_rw(&path).expect("open");
let fs = Filesystem::mount(Arc::new(dev)).expect("mount");
let mut reader = |ino: u32| fs.read_inode_verified(ino).map(|(i, _)| i);
let Ok(ino) = fs_ext4::path::lookup(fs.dev.as_ref(), &fs.sb, &mut reader, "/test.txt")
else {
return;
};
let _ = fs.apply_truncate_grow(ino, u64::MAX);
let _ = fs.apply_truncate_grow(ino, u64::MAX / 2);
let _ = fs.apply_truncate_shrink(ino, 0);
let _ = fs.apply_truncate_shrink(ino, u64::MAX); let _ = fs.apply_truncate_grow(ino, 1); });
assert!(result.is_ok(), "truncate edge cases panicked");
fs::remove_file(path).ok();
}
#[test]
fn writes_to_nonexistent_paths_never_panic() {
let Some(path) = copy_to_tmp("ext4-basic.img", "nonexistent") else {
return;
};
let result = try_call(|| {
let dev = FileDevice::open_rw(&path).expect("open");
let fs = Filesystem::mount(Arc::new(dev)).expect("mount");
let _ = fs.apply_chmod("/does/not/exist", 0o644);
let _ = fs.apply_chown("/no/such/path", 1, 1);
let _ = fs.apply_setxattr("/missing", "user.x", b"v");
let _ = fs.apply_removexattr("/missing", "user.x");
let _ = fs.apply_unlink("/missing");
let _ = fs.apply_rmdir("/missing");
let _ = fs.apply_chmod("/\u{1F4A9}\u{0000}", 0o644);
let _ = fs.apply_chmod("/../../../etc/passwd", 0o000);
let long = "/".to_string() + &"a".repeat(1024);
let _ = fs.apply_chmod(&long, 0o644);
});
assert!(result.is_ok(), "writes on nonexistent paths panicked");
fs::remove_file(path).ok();
}
#[test]
fn read_only_device_rejects_all_writes_cleanly() {
let Some(path) = copy_to_tmp("ext4-basic.img", "ro_writes") else {
return;
};
let result = try_call(|| {
let dev = FileDevice::open(&path).expect("open ro");
let fs = Filesystem::mount(Arc::new(dev)).expect("mount");
assert!(fs.apply_chmod("/test.txt", 0o644).is_err());
assert!(fs.apply_chown("/test.txt", 1, 1).is_err());
assert!(fs.apply_unlink("/test.txt").is_err());
assert!(fs.apply_setxattr("/test.txt", "user.x", b"v").is_err());
assert!(fs.apply_removexattr("/test.txt", "user.x").is_err());
});
assert!(result.is_ok(), "RO device write rejection panicked");
fs::remove_file(path).ok();
}