use fs_ext4::block_io::FileDevice;
use fs_ext4::extent;
use fs_ext4::features;
use fs_ext4::fs::Filesystem;
use fs_ext4::fsck::Anomaly;
use std::fs;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
fn copy_to_tmp(name: &str, slot: &str) -> String {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let src = fs_ext4_test_support::fixture(env!("CARGO_MANIFEST_DIR"), name);
let dst = fs_ext4_test_support::temp_path!(
"fs_ext4_wrong_dotdot_{}_{slot}_{n}.img",
std::process::id()
);
fs::copy(&src, &dst).unwrap_or_else(|e| panic!("copy {src} -> {dst}: {e}"));
dst
}
fn find_dirent_slot(fs: &Filesystem, dir_ino: u32, name: &[u8]) -> (u64, usize, u32) {
let (inode, _) = fs.read_inode_verified(dir_ino).expect("read dir inode");
let has_ft = fs.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
let bs = fs.sb.block_size();
let total = inode.size.div_ceil(bs as u64);
let mut buf = vec![0u8; bs as usize];
for logical in 0..total {
let Some(phys) =
extent::map_logical(&inode.block, fs.dev.as_ref(), bs, logical).expect("map")
else {
continue;
};
fs.dev.read_at(phys * bs as u64, &mut buf).expect("read");
let mut off = 0usize;
while off + 8 <= buf.len() {
let cur_inode = u32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
let rec_len = u16::from_le_bytes(buf[off + 4..off + 6].try_into().unwrap()) as usize;
if rec_len < 8 || !rec_len.is_multiple_of(4) || off + rec_len > buf.len() {
break;
}
if cur_inode != 0 {
let cur_name_lo = buf[off + 6];
let cur_type_or_hi = buf[off + 7];
let cur_name_len = if has_ft {
cur_name_lo as usize
} else {
((cur_type_or_hi as usize) << 8) | cur_name_lo as usize
};
if 8 + cur_name_len <= rec_len && &buf[off + 8..off + 8 + cur_name_len] == name {
return (phys, off, cur_inode);
}
}
off += rec_len;
}
}
panic!("dirent {:?} not found in dir ino {}", name, dir_ino);
}
fn poke_inode_field_raw(
image_path: &str,
block_size: u32,
phys_block: u64,
off: usize,
new_inode: u32,
) {
use std::io::{Seek, SeekFrom, Write};
let mut f = fs::OpenOptions::new()
.read(true)
.write(true)
.open(image_path)
.expect("open raw");
f.seek(SeekFrom::Start(phys_block * block_size as u64 + off as u64))
.expect("seek");
f.write_all(&new_inode.to_le_bytes()).expect("write");
}
fn fix_dir_csum_after_poke(image_path: &str, fs: &Filesystem, dir_ino: u32, phys_block: u64) {
use std::io::{Seek, SeekFrom, Write};
if !fs.csum.enabled {
return;
}
let bs = fs.sb.block_size() as usize;
let block = fs.read_block(phys_block).expect("read block");
if !fs_ext4::dir::has_csum_tail(&block) {
return;
}
let (inode, _) = fs.read_inode_verified(dir_ino).expect("read inode");
let mut c = fs_ext4::checksum::linux_crc32c(fs.csum.seed, &dir_ino.to_le_bytes());
c = fs_ext4::checksum::linux_crc32c(c, &inode.generation.to_le_bytes());
c = fs_ext4::checksum::linux_crc32c(c, &block[..bs - 12]);
let mut f = fs::OpenOptions::new()
.read(true)
.write(true)
.open(image_path)
.expect("open raw");
f.seek(SeekFrom::Start(phys_block * bs as u64 + (bs as u64 - 4)))
.expect("seek");
f.write_all(&c.to_le_bytes()).expect("write csum");
}
#[test]
fn audit_flags_wrong_dotdot_on_non_root_dir() {
let path = copy_to_tmp("ext4-basic.img", "wrongdotdot");
let subdir_ino;
{
let dev = FileDevice::open_rw(&path).expect("open rw");
let fs = Filesystem::mount(Arc::new(dev)).expect("mount");
subdir_ino = fs
.apply_mkdir("/wrongdotdot_test", 0o755)
.expect("mkdir test dir");
}
let dotdot_slot;
let bs;
{
let dev = FileDevice::open_rw(&path).expect("open rw 2");
let fs = Filesystem::mount(Arc::new(dev)).expect("mount 2");
dotdot_slot = find_dirent_slot(&fs, subdir_ino, b"..");
bs = fs.sb.block_size();
assert_eq!(
dotdot_slot.2,
fs_ext4::path::EXT4_ROOT_INODE,
"fresh subdir's .. must point at root before patch"
);
}
let fake_parent: u32 = 99;
poke_inode_field_raw(&path, bs, dotdot_slot.0, dotdot_slot.1, fake_parent);
{
let dev = FileDevice::open_rw(&path).expect("open rw 3");
let fs = Filesystem::mount(Arc::new(dev)).expect("mount 3");
fix_dir_csum_after_poke(&path, &fs, subdir_ino, dotdot_slot.0);
}
{
let dev = FileDevice::open(&path).expect("open ro audit");
let fs = Filesystem::mount(Arc::new(dev)).expect("mount audit");
let report = fs.audit(u32::MAX, u32::MAX).expect("audit");
let detected = report.anomalies.iter().any(|a| {
matches!(a,
Anomaly::WrongDotDot { dir_ino, claims, actual_parent }
if *dir_ino == subdir_ino
&& *claims == fake_parent
&& *actual_parent == fs_ext4::path::EXT4_ROOT_INODE
)
});
assert!(
detected,
"expected WrongDotDot for subdir {subdir_ino} (got {:#?})",
report.anomalies
);
}
let _ = std::fs::remove_file(&path);
}