use crate::bgd;
use crate::dir::{self, DirBlockIter, DirEntryType};
use crate::error::{Error, Result};
use crate::features;
use crate::fs::{BlockBuffer, Filesystem};
use crate::inode::Inode;
use crate::runtime::Stopwatch;
use crate::superblock::Superblock;
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Anomaly {
LinkCountTooLow {
ino: u32,
stored: u16,
observed: u32,
},
LinkCountTooHigh {
ino: u32,
stored: u16,
observed: u32,
},
DanglingEntry {
parent_ino: u32,
child_ino: u32,
observed: u32,
},
WrongDotDot {
dir_ino: u32,
claims: u32,
actual_parent: u32,
},
BogusEntry {
parent_ino: u32,
child_ino: u32,
name: Vec<u8>,
},
BlockGroupFreeCountDrift {
group_index: u32,
stored_blocks: u32,
observed_blocks: u32,
stored_inodes: u32,
observed_inodes: u32,
},
SuperblockFreeCountDrift {
stored_blocks: u64,
observed_blocks: u64,
stored_inodes: u32,
observed_inodes: u32,
},
DuplicateDirentForDirInode {
ino: u32,
dirents: Vec<(u32, String)>,
},
DirBlockChecksumMismatch {
dir_ino: u32,
logical_block: u64,
htree: bool,
},
UnreadableDirectory { ino: u32, reason: String },
}
#[derive(Debug, Clone, Default)]
pub struct AuditReport {
pub anomalies: Vec<Anomaly>,
pub inodes_visited: u32,
pub entries_scanned: u64,
pub directories_scanned: u32,
pub anomalies_count: u64,
pub initial_anomalies_count: u64,
pub repaired_count: u64,
}
impl AuditReport {
pub fn is_clean(&self) -> bool {
self.anomalies_count == 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum FsckPhase {
Superblock = 0,
Journal = 1,
Directory = 2,
Inodes = 3,
Finalize = 4,
}
impl FsckPhase {
pub fn name(self) -> &'static str {
match self {
FsckPhase::Superblock => "superblock",
FsckPhase::Journal => "journal",
FsckPhase::Directory => "directory",
FsckPhase::Inodes => "inodes",
FsckPhase::Finalize => "finalize",
}
}
}
pub fn audit(
fs: &Filesystem,
max_dirs_visited: u32,
max_entries_per_dir: u32,
) -> Result<AuditReport> {
let mut report = AuditReport::default();
let mut collected: Vec<Anomaly> = Vec::new();
audit_inner(
fs,
max_dirs_visited,
max_entries_per_dir,
&mut |_, _, _| {},
&mut |a| collected.push(a.clone()),
&mut report,
)?;
report.anomalies = collected;
Ok(report)
}
pub fn audit_with_callbacks<P, F>(
fs: &Filesystem,
max_dirs_visited: u32,
max_entries_per_dir: u32,
mut on_progress: P,
mut on_finding: F,
) -> Result<AuditReport>
where
P: FnMut(FsckPhase, u64, u64),
F: FnMut(&Anomaly),
{
let mut report = AuditReport::default();
on_progress(FsckPhase::Superblock, 0, 1);
on_progress(FsckPhase::Superblock, 1, 1);
audit_inner(
fs,
max_dirs_visited,
max_entries_per_dir,
&mut on_progress,
&mut on_finding,
&mut report,
)?;
Ok(report)
}
pub(crate) fn refuse_bigalloc(fs: &Filesystem) -> Result<()> {
if fs.sb.feature_ro_compat & crate::features::RoCompat::BIGALLOC.bits() != 0 {
return Err(crate::error::Error::Unsupported(
"bigalloc: bitmaps and group free counts are in clusters, which the audit does not model",
));
}
Ok(())
}
fn audit_inner(
fs: &Filesystem,
max_dirs_visited: u32,
max_entries_per_dir: u32,
on_progress: &mut dyn FnMut(FsckPhase, u64, u64),
on_finding: &mut dyn FnMut(&Anomaly),
report: &mut AuditReport,
) -> Result<()> {
refuse_bigalloc(fs)?;
let mut observed: HashMap<u32, u32> = HashMap::new();
let mut parent_claim: HashMap<u32, u32> = HashMap::new();
let mut actual_parent: HashMap<u32, u32> = HashMap::new();
let mut incomplete_dirs: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut unreadable_dirs: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut dirent_index: HashMap<u32, Vec<(u32, Vec<u8>)>> = HashMap::new();
let mut work: Vec<(u32, u32, Vec<u8>)> = Vec::new();
work.push((
crate::path::EXT4_ROOT_INODE,
crate::path::EXT4_ROOT_INODE,
Vec::new(),
));
let mut visited: std::collections::HashSet<u32> = std::collections::HashSet::new();
let has_filetype = fs.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
let block_size = fs.sb.block_size();
on_progress(FsckPhase::Directory, 0, work.len() as u64);
while let Some((dir_ino, parent_ino, dirent_name)) = work.pop() {
if report.directories_scanned >= max_dirs_visited {
incomplete_dirs.insert(dir_ino);
break;
}
if visited.contains(&dir_ino) {
continue;
}
actual_parent.entry(dir_ino).or_insert(parent_ino);
report.directories_scanned += 1;
let (inode, raw) = match fs.read_inode_verified(dir_ino) {
Ok(p) => p,
Err(e) => {
incomplete_dirs.insert(dir_ino);
unreadable_dirs.insert(dir_ino);
on_finding(&Anomaly::UnreadableDirectory {
ino: dir_ino,
reason: e.to_string(),
});
report.anomalies_count += 1;
emit_dir_progress(on_progress, report.directories_scanned, work.len());
continue;
}
};
if !inode.is_dir() {
let a = Anomaly::BogusEntry {
parent_ino,
child_ino: dir_ino,
name: dirent_name.clone(),
};
on_finding(&a);
report.anomalies_count += 1;
emit_dir_progress(on_progress, report.directories_scanned, work.len());
continue;
}
visited.insert(dir_ino);
let mut bad_checksums: Vec<(u64, bool)> = Vec::new();
let collected = collect_dir_entries_checked(
fs,
dir_ino,
&inode,
&raw,
has_filetype,
block_size,
Some(&mut |logical, htree| bad_checksums.push((logical, htree))),
);
for (logical_block, htree) in bad_checksums {
let a = Anomaly::DirBlockChecksumMismatch {
dir_ino,
logical_block,
htree,
};
on_finding(&a);
report.anomalies_count += 1;
}
let entries = match collected {
Ok(e) => e,
Err(e) => {
incomplete_dirs.insert(dir_ino);
unreadable_dirs.insert(dir_ino);
on_finding(&Anomaly::UnreadableDirectory {
ino: dir_ino,
reason: e.to_string(),
});
report.anomalies_count += 1;
emit_dir_progress(on_progress, report.directories_scanned, work.len());
continue;
}
};
let mut truncated = false;
for (n_scanned, entry) in (0u32..).zip(entries) {
if n_scanned >= max_entries_per_dir {
truncated = true;
break;
}
report.entries_scanned += 1;
if entry.name == b"." {
*observed.entry(dir_ino).or_insert(0) += 1;
continue;
}
if entry.name == b".." {
parent_claim.insert(dir_ino, entry.inode);
*observed.entry(entry.inode).or_insert(0) += 1;
continue;
}
*observed.entry(entry.inode).or_insert(0) += 1;
dirent_index
.entry(entry.inode)
.or_default()
.push((dir_ino, entry.name.clone()));
if matches!(entry.file_type, DirEntryType::Directory) {
work.push((entry.inode, dir_ino, entry.name.clone()));
}
}
if truncated {
incomplete_dirs.insert(dir_ino);
}
emit_dir_progress(on_progress, report.directories_scanned, work.len());
}
report.inodes_visited = observed.len() as u32;
let inodes_total = observed.len() as u64;
on_progress(FsckPhase::Inodes, 0, inodes_total.max(1));
let have_incomplete = !incomplete_dirs.is_empty();
let dir_nlink = fs.sb.feature_ro_compat & crate::features::RoCompat::DIR_NLINK.bits() != 0;
let mut inodes_done: u64 = 0;
let mut last_tick = Stopwatch::start();
let tick = Duration::from_millis(500);
for (&ino, &count) in observed.iter() {
match fs.read_inode_verified(ino) {
Ok((inode, _)) => {
let stored = inode.links_count;
if stored == 0 {
let a = Anomaly::DanglingEntry {
parent_ino: 0,
child_ino: ino,
observed: count,
};
on_finding(&a);
report.anomalies_count += 1;
continue;
}
let uncounted = stored == 1 && inode.is_dir() && dir_nlink;
if (stored as u32) < count && !uncounted {
let a = Anomaly::LinkCountTooLow {
ino,
stored,
observed: count,
};
on_finding(&a);
report.anomalies_count += 1;
}
if (stored as u32) > count && !have_incomplete {
let a = Anomaly::LinkCountTooHigh {
ino,
stored,
observed: count,
};
on_finding(&a);
report.anomalies_count += 1;
}
}
Err(_) if unreadable_dirs.contains(&ino) => {
}
Err(_) => {
let a = Anomaly::DanglingEntry {
parent_ino: 0,
child_ino: ino,
observed: 0,
};
on_finding(&a);
report.anomalies_count += 1;
}
}
inodes_done += 1;
if last_tick.elapsed() >= tick {
on_progress(FsckPhase::Inodes, inodes_done, inodes_total.max(1));
last_tick = Stopwatch::start();
}
}
let mut dup_keys: Vec<u32> = dirent_index
.iter()
.filter_map(|(ino, refs)| if refs.len() > 1 { Some(*ino) } else { None })
.collect();
dup_keys.sort_unstable();
for ino in dup_keys {
let is_dir = match fs.read_inode_verified(ino) {
Ok((inode, _)) => inode.is_dir(),
Err(_) => continue,
};
if !is_dir {
continue;
}
let mut refs = dirent_index.get(&ino).cloned().unwrap_or_default();
refs.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
let dirents: Vec<(u32, String)> = refs
.into_iter()
.map(|(p, n)| (p, String::from_utf8_lossy(&n).into_owned()))
.collect();
let a = Anomaly::DuplicateDirentForDirInode { ino, dirents };
on_finding(&a);
report.anomalies_count += 1;
}
for (&dir_ino, &claimed) in parent_claim.iter() {
let truth = if dir_ino == crate::path::EXT4_ROOT_INODE {
crate::path::EXT4_ROOT_INODE
} else {
match actual_parent.get(&dir_ino) {
Some(&p) => p,
None => continue,
}
};
if claimed != truth {
let a = Anomaly::WrongDotDot {
dir_ino,
claims: claimed,
actual_parent: truth,
};
on_finding(&a);
report.anomalies_count += 1;
}
}
on_progress(FsckPhase::Inodes, inodes_total.max(1), inodes_total.max(1));
audit_free_counts(fs, on_finding, report)?;
on_progress(FsckPhase::Finalize, 0, 1);
on_progress(FsckPhase::Finalize, 1, 1);
Ok(())
}
fn audit_free_counts(
fs: &Filesystem,
on_finding: &mut dyn FnMut(&Anomaly),
report: &mut AuditReport,
) -> Result<()> {
let bpg = fs.sb.blocks_per_group as u64;
let ipg = fs.sb.inodes_per_group as u64;
let total_blocks = fs.sb.blocks_count;
let first_data = fs.sb.first_data_block as u64;
let live_groups = bgd::read_all(fs.dev.as_ref(), &fs.sb, &fs.csum)?;
let live_sb = Superblock::read(fs.dev.as_ref())?;
let mut sum_free_blocks: u64 = 0;
let mut sum_free_inodes: u64 = 0;
for (gi, bg) in live_groups.iter().enumerate() {
let group_first_block = first_data + gi as u64 * bpg;
let group_block_count = std::cmp::min(bpg, total_blocks.saturating_sub(group_first_block));
let block_bitmap = if bg.flags().contains(bgd::BgdFlags::BLOCK_UNINIT) {
crate::alloc::uninit_block_bitmap(&fs.sb, &live_groups, gi as u32)
} else {
fs.read_block(bg.block_bitmap)?
};
let observed_blocks = count_zero_bits_le(&block_bitmap, group_block_count as u32);
let observed_inodes = if bg.flags().contains(bgd::BgdFlags::INODE_UNINIT) {
ipg as u32
} else {
let inode_bitmap = fs.read_block(bg.inode_bitmap)?;
count_zero_bits_le(&inode_bitmap, ipg as u32)
};
sum_free_blocks += observed_blocks as u64;
sum_free_inodes += observed_inodes as u64;
if observed_blocks != bg.free_blocks_count || observed_inodes != bg.free_inodes_count {
let a = Anomaly::BlockGroupFreeCountDrift {
group_index: gi as u32,
stored_blocks: bg.free_blocks_count,
observed_blocks,
stored_inodes: bg.free_inodes_count,
observed_inodes,
};
on_finding(&a);
report.anomalies_count += 1;
}
}
if sum_free_blocks != live_sb.free_blocks_count
|| (sum_free_inodes as u32) != live_sb.free_inodes_count
{
let a = Anomaly::SuperblockFreeCountDrift {
stored_blocks: live_sb.free_blocks_count,
observed_blocks: sum_free_blocks,
stored_inodes: live_sb.free_inodes_count,
observed_inodes: sum_free_inodes as u32,
};
on_finding(&a);
report.anomalies_count += 1;
}
Ok(())
}
fn count_zero_bits_le(buf: &[u8], total_bits: u32) -> u32 {
let full_bytes = (total_bits / 8) as usize;
let mut free: u32 = 0;
for i in 0..full_bytes {
if i >= buf.len() {
break;
}
free += buf[i].count_zeros();
}
let leftover_bits = total_bits % 8;
if leftover_bits > 0 && full_bytes < buf.len() {
let last = buf[full_bytes];
let mask = (1u8 << leftover_bits) - 1;
let ones_in_used_bits = (last & mask).count_ones();
free += leftover_bits - ones_in_used_bits;
}
free
}
fn emit_dir_progress(
on_progress: &mut dyn FnMut(FsckPhase, u64, u64),
scanned: u32,
queue_len: usize,
) {
let done = scanned as u64;
let total = done + queue_len as u64;
on_progress(FsckPhase::Directory, done, total);
}
fn collect_dir_entries(
fs: &Filesystem,
ino: u32,
inode: &Inode,
inode_raw: &[u8],
has_filetype: bool,
block_size: u32,
) -> Result<Vec<crate::dir::DirEntry>> {
collect_dir_entries_checked(fs, ino, inode, inode_raw, has_filetype, block_size, None)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DirBlockChecksum {
Unchecked,
Matches,
Mismatch { htree: bool },
}
fn dir_block_checksum(
fs: &Filesystem,
dir_ino: u32,
dir: &Inode,
logical: u64,
block: &[u8],
) -> DirBlockChecksum {
if !fs.csum.enabled {
return DirBlockChecksum::Unchecked;
}
if Filesystem::is_htree_index_block(dir, logical, block) {
let count_offset = if logical == 0 {
if block.get(29) != Some(&8) {
return DirBlockChecksum::Unchecked;
}
32
} else {
8
};
let Some(pair) = block.get(count_offset..count_offset + 2) else {
return DirBlockChecksum::Unchecked;
};
let limit = u16::from_le_bytes([pair[0], pair[1]]) as usize;
if count_offset + limit * 8 + 8 != block.len() {
return DirBlockChecksum::Unchecked;
}
return match fs
.csum
.verify_dx_tail(dir_ino, dir.generation, block, count_offset)
{
Some(true) => DirBlockChecksum::Matches,
Some(false) => DirBlockChecksum::Mismatch { htree: true },
None => DirBlockChecksum::Unchecked,
};
}
if !dir::has_csum_tail(block) {
return DirBlockChecksum::Unchecked;
}
if fs
.csum
.verify_dir_entry_tail(dir_ino, dir.generation, block)
{
DirBlockChecksum::Matches
} else {
DirBlockChecksum::Mismatch { htree: false }
}
}
fn may_restamp(fs: &Filesystem, dir_ino: u32, dir: &Inode, logical: u64, block: &[u8]) -> bool {
!matches!(
dir_block_checksum(fs, dir_ino, dir, logical, block),
DirBlockChecksum::Mismatch { .. }
)
}
fn dir_block_passes_checks(block: &[u8], has_filetype: bool) -> bool {
let Some(end) = block.len().checked_sub(12) else {
return false;
};
let mut off = 0usize;
while off < end {
if off + 8 > end {
return false;
}
let rec_len = u16::from_le_bytes([block[off + 4], block[off + 5]]) as usize;
if rec_len < 8 || !rec_len.is_multiple_of(4) || off + rec_len > end {
return false;
}
let name_len = if has_filetype {
block[off + 6] as usize
} else {
((block[off + 7] as usize) << 8) | block[off + 6] as usize
};
if 8 + name_len > rec_len {
return false;
}
off += rec_len;
}
off == end
}
fn collect_dir_entries_checked(
fs: &Filesystem,
dir_ino: u32,
inode: &Inode,
inode_raw: &[u8],
has_filetype: bool,
block_size: u32,
mut on_bad_checksum: Option<&mut dyn FnMut(u64, bool)>,
) -> Result<Vec<crate::dir::DirEntry>> {
if inode.has_inline_data() {
return crate::inline_data::read_dir(
fs.dev.as_ref(),
dir_ino,
inode,
inode_raw,
fs.sb.inode_size,
block_size,
has_filetype,
);
}
let mut entries = Vec::new();
let total_blocks = inode.size.div_ceil(block_size as u64);
let mut buf = vec![0u8; block_size as usize];
for logical in 0..total_blocks {
let Some(phys) = crate::indirect::map_logical_any(
&inode.block,
inode.flags,
fs.dev.as_ref(),
block_size,
logical,
)?
else {
continue;
};
let offset = phys
.checked_mul(block_size as u64)
.ok_or(Error::Corrupt("audit: dir block offset overflow"))?;
fs.dev.read_at(offset, &mut buf)?;
if let Some(on_bad_checksum) = on_bad_checksum.as_mut() {
if let DirBlockChecksum::Mismatch { htree } =
dir_block_checksum(fs, dir_ino, inode, logical, &buf)
{
on_bad_checksum(logical, htree);
}
}
for entry in DirBlockIter::new(&buf, has_filetype) {
match entry {
Ok(e) => entries.push(e),
Err(_) if logical == 0 => break,
Err(e) => return Err(e),
}
}
}
Ok(entries)
}
pub fn audit_with_repair<P, F>(
fs: &Filesystem,
max_dirs_visited: u32,
max_entries_per_dir: u32,
mut on_progress: P,
mut on_finding: F,
repair: bool,
) -> Result<AuditReport>
where
P: FnMut(FsckPhase, u64, u64),
F: FnMut(&Anomaly),
{
if repair {
fs.refuse_write()?;
}
let mut report = AuditReport::default();
on_progress(FsckPhase::Superblock, 0, 1);
on_progress(FsckPhase::Superblock, 1, 1);
let mut collected: Vec<Anomaly> = Vec::new();
audit_inner(
fs,
max_dirs_visited,
max_entries_per_dir,
&mut on_progress,
&mut |a| {
on_finding(a);
collected.push(a.clone());
},
&mut report,
)?;
report.initial_anomalies_count = report.anomalies_count;
if !repair {
report.anomalies = collected;
return Ok(report);
}
let surviving_parent_after_dedup: HashMap<u32, u32> = collected
.iter()
.filter_map(|a| match a {
Anomaly::DuplicateDirentForDirInode { ino, dirents } if !dirents.is_empty() => {
Some((*ino, dirents[0].0))
}
_ => None,
})
.collect();
for finding in &collected {
if let Anomaly::DirBlockChecksumMismatch {
dir_ino,
logical_block,
htree,
} = finding
{
repair_dir_block_checksum(fs, *dir_ino, *logical_block, *htree, &mut report)?;
}
}
let before_dirent_edits = report.repaired_count;
for finding in &collected {
match finding {
Anomaly::DuplicateDirentForDirInode { ino, dirents } => {
repair_duplicate_dir_inode(fs, *ino, dirents, &mut report)?;
}
Anomaly::WrongDotDot {
dir_ino,
claims: _,
actual_parent,
} => {
let target_parent = surviving_parent_after_dedup
.get(dir_ino)
.copied()
.unwrap_or(*actual_parent);
repair_wrong_dotdot(fs, *dir_ino, target_parent, &mut report)?;
}
Anomaly::BogusEntry {
parent_ino,
child_ino,
name,
} => {
repair_bogus_entry(fs, *parent_ino, *child_ino, name, &mut report)?;
}
_ => {}
}
}
let counts_from: Vec<Anomaly> = if report.repaired_count == before_dirent_edits {
collected.clone()
} else {
let mut recount = Vec::new();
audit_inner(
fs,
max_dirs_visited,
max_entries_per_dir,
&mut on_progress,
&mut |a| recount.push(a.clone()),
&mut AuditReport::default(),
)?;
recount
};
for finding in &counts_from {
match finding {
Anomaly::LinkCountTooLow {
ino,
stored: _,
observed,
}
| Anomaly::LinkCountTooHigh {
ino,
stored: _,
observed,
} => {
repair_link_count(fs, *ino, *observed, &mut report)?;
}
Anomaly::DanglingEntry {
parent_ino: _,
child_ino,
observed,
} => {
repair_link_count(fs, *child_ino, *observed, &mut report)?;
}
Anomaly::BlockGroupFreeCountDrift {
group_index,
stored_blocks,
observed_blocks,
stored_inodes,
observed_inodes,
} => {
repair_block_group_free_counts(
fs,
*group_index,
*stored_blocks,
*observed_blocks,
*stored_inodes,
*observed_inodes,
&mut report,
)?;
}
Anomaly::SuperblockFreeCountDrift {
stored_blocks,
observed_blocks,
stored_inodes,
observed_inodes,
} => {
repair_superblock_free_counts(
fs,
*stored_blocks,
*observed_blocks,
*stored_inodes,
*observed_inodes,
&mut report,
)?;
}
Anomaly::DirBlockChecksumMismatch { .. }
| Anomaly::UnreadableDirectory { .. }
| Anomaly::DuplicateDirentForDirInode { .. }
| Anomaly::WrongDotDot { .. }
| Anomaly::BogusEntry { .. } => {}
}
}
let mut post_report = AuditReport::default();
let mut remaining: Vec<Anomaly> = Vec::new();
audit_inner(
fs,
max_dirs_visited,
max_entries_per_dir,
&mut on_progress,
&mut |a| {
remaining.push(a.clone());
},
&mut post_report,
)?;
report.anomalies = remaining;
report.anomalies_count = post_report.anomalies_count;
Ok(report)
}
fn repair_duplicate_dir_inode(
fs: &Filesystem,
ino: u32,
dirents: &[(u32, String)],
report: &mut AuditReport,
) -> Result<()> {
if dirents.len() < 2 {
return Ok(());
}
let has_ft = fs.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
let bs = fs.sb.block_size();
let mut any_removed = false;
for (parent_ino, name) in dirents.iter().skip(1) {
let (parent_inode, _parent_raw) = fs.read_inode_verified(*parent_ino)?;
if !parent_inode.is_dir() {
continue;
}
let mut buf = BlockBuffer::new(bs);
let parent_blocks = parent_inode.size.div_ceil(bs as u64);
let mut removed = false;
for logical in 0..parent_blocks {
let Some(phys) = fs.map_inode_logical(&parent_inode, logical)? else {
continue;
};
let block = buf.get_mut(fs, phys)?;
if !may_restamp(fs, *parent_ino, &parent_inode, logical, block) {
let here = DirBlockIter::new(block, has_ft)
.map_while(|e| e.ok())
.any(|e| e.name == name.as_bytes());
if here {
break;
}
continue;
}
let reserved_tail = if fs.csum.enabled && dir::has_csum_tail(block) {
12
} else {
0
};
if dir::remove_entry_from_block(block, name.as_bytes(), has_ft, reserved_tail)? {
if fs.csum.enabled && reserved_tail == 12 {
fs.csum
.patch_dir_entry_tail(*parent_ino, parent_inode.generation, block);
}
removed = true;
break;
}
}
if !removed {
continue;
}
fs.commit_block_buffer(buf)?;
any_removed = true;
}
if any_removed {
report.repaired_count += 1;
}
let (kept_inode, mut kept_raw) = fs.read_inode_verified(ino)?;
if !kept_inode.is_dir() {
return Ok(());
}
let subdir_count = count_subdirs(fs, ino, &kept_inode, &kept_raw, has_ft, bs)?;
let new_nlink: u16 = 2u16.saturating_add(subdir_count.min(u16::MAX as u32 - 2) as u16);
kept_raw[0x1A..0x1C].copy_from_slice(&new_nlink.to_le_bytes());
finalize_and_commit_inode(fs, ino, kept_inode.generation, &mut kept_raw)?;
Ok(())
}
fn count_subdirs(
fs: &Filesystem,
dir_ino: u32,
dir_inode: &Inode,
dir_raw: &[u8],
has_filetype: bool,
block_size: u32,
) -> Result<u32> {
let entries = collect_dir_entries(fs, dir_ino, dir_inode, dir_raw, has_filetype, block_size)?;
let mut n = 0u32;
for e in entries {
if e.name == b"." || e.name == b".." {
continue;
}
if matches!(e.file_type, DirEntryType::Directory) {
if let Ok((child_inode, _)) = fs.read_inode_verified(e.inode) {
if child_inode.is_dir() {
n = n.saturating_add(1);
}
}
}
}
Ok(n)
}
fn repair_link_count(
fs: &Filesystem,
ino: u32,
observed: u32,
report: &mut AuditReport,
) -> Result<()> {
if observed == 0 || observed > u16::MAX as u32 {
return Ok(());
}
let (inode, mut raw) = fs.read_inode_verified(ino)?;
let count = if inode.is_dir() && observed > crate::fs::MAX_LINKS as u32 {
if fs.sb.feature_ro_compat & crate::features::RoCompat::DIR_NLINK.bits() == 0 {
return Ok(());
}
1u16
} else {
observed as u16
};
raw[0x1A..0x1C].copy_from_slice(&count.to_le_bytes());
finalize_and_commit_inode(fs, ino, inode.generation, &mut raw)?;
report.repaired_count += 1;
Ok(())
}
fn repair_wrong_dotdot(
fs: &Filesystem,
dir_ino: u32,
actual_parent: u32,
report: &mut AuditReport,
) -> Result<()> {
let (dir_inode, _raw) = fs.read_inode_verified(dir_ino)?;
if !dir_inode.is_dir() {
return Ok(());
}
let bs = fs.sb.block_size();
let Some(phys) = fs.map_inode_logical(&dir_inode, 0)? else {
return Ok(());
};
let mut buf = BlockBuffer::new(bs);
let block = buf.get_mut(fs, phys)?;
if !may_restamp(fs, dir_ino, &dir_inode, 0, block) {
return Ok(());
}
let index = Filesystem::is_htree_index_block(&dir_inode, 0, block);
if index && block.get(29) != Some(&8) {
return Ok(());
}
let reserved_tail = if !index && fs.csum.enabled && dir::has_csum_tail(block) {
12
} else {
0
};
let usable_end = block.len().saturating_sub(reserved_tail);
let mut off = 0usize;
let mut found = false;
while off + 8 <= usable_end {
let rec_len = u16::from_le_bytes([block[off + 4], block[off + 5]]) as usize;
if rec_len == 0 || off + rec_len > usable_end {
break;
}
let name_len = block[off + 6] as usize;
let name_start = off + 8;
let name_end = name_start + name_len;
if name_end <= off + rec_len && &block[name_start..name_end] == b".." {
block[off..off + 4].copy_from_slice(&actual_parent.to_le_bytes());
found = true;
break;
}
off += rec_len;
}
if !found {
return Ok(());
}
if index {
fs.csum
.patch_dx_tail(dir_ino, dir_inode.generation, block, 32);
} else if fs.csum.enabled && reserved_tail == 12 {
fs.csum
.patch_dir_entry_tail(dir_ino, dir_inode.generation, block);
}
fs.commit_block_buffer(buf)?;
report.repaired_count += 1;
Ok(())
}
fn repair_bogus_entry(
fs: &Filesystem,
parent_ino: u32,
child_ino: u32,
name: &[u8],
report: &mut AuditReport,
) -> Result<()> {
let has_ft = fs.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
if !has_ft {
return Ok(());
}
if name.is_empty() {
return Ok(());
}
let (parent_inode, _parent_raw) = fs.read_inode_verified(parent_ino)?;
if !parent_inode.is_dir() {
return Ok(());
}
let child_filetype: DirEntryType = match fs.read_inode_verified(child_ino) {
Ok((child_inode, _)) => match DirEntryType::from_mode(child_inode.mode) {
DirEntryType::Directory | DirEntryType::Unknown => return Ok(()),
t => t,
},
Err(_) => return Ok(()),
};
let bs = fs.sb.block_size();
let parent_blocks = parent_inode.size.div_ceil(bs as u64);
let mut buf = BlockBuffer::new(bs);
let mut found = false;
for logical in 0..parent_blocks {
let Some(phys) = fs.map_inode_logical(&parent_inode, logical)? else {
continue;
};
let block = buf.get_mut(fs, phys)?;
let reserved_tail = if fs.csum.enabled && dir::has_csum_tail(block) {
12
} else {
0
};
let usable_end = block.len().saturating_sub(reserved_tail);
let mut off = 0usize;
let mut hit_off: Option<usize> = None;
while off + 8 <= usable_end {
let cur_inode =
u32::from_le_bytes([block[off], block[off + 1], block[off + 2], block[off + 3]]);
let rec_len = u16::from_le_bytes([block[off + 4], block[off + 5]]) as usize;
if rec_len == 0 || off + rec_len > usable_end {
break;
}
let name_len = block[off + 6] as usize;
if cur_inode == child_ino
&& 8 + name_len <= rec_len
&& off + 8 + name_len <= usable_end
&& &block[off + 8..off + 8 + name_len] == name
{
hit_off = Some(off);
break;
}
off += rec_len;
}
if let Some(off) = hit_off {
if !may_restamp(fs, parent_ino, &parent_inode, logical, block) {
return Ok(());
}
block[off + 7] = child_filetype as u8;
if fs.csum.enabled && reserved_tail == 12 {
fs.csum
.patch_dir_entry_tail(parent_ino, parent_inode.generation, block);
}
found = true;
break;
}
}
if found {
fs.commit_block_buffer(buf)?;
report.repaired_count += 1;
}
Ok(())
}
fn repair_dir_block_checksum(
fs: &Filesystem,
dir_ino: u32,
logical: u64,
htree: bool,
report: &mut AuditReport,
) -> Result<()> {
if htree {
return Ok(());
}
let (dir_inode, _raw) = fs.read_inode_verified(dir_ino)?;
if !dir_inode.is_dir() {
return Ok(());
}
let Some(phys) = fs.map_inode_logical(&dir_inode, logical)? else {
return Ok(());
};
let has_ft = fs.sb.feature_incompat & features::Incompat::FILETYPE.bits() != 0;
let mut buf = BlockBuffer::new(fs.sb.block_size());
let block = buf.get_mut(fs, phys)?;
if dir_block_checksum(fs, dir_ino, &dir_inode, logical, block)
!= (DirBlockChecksum::Mismatch { htree: false })
|| !dir_block_passes_checks(block, has_ft)
{
return Ok(());
}
fs.csum
.patch_dir_entry_tail(dir_ino, dir_inode.generation, block);
fs.commit_block_buffer(buf)?;
report.repaired_count += 1;
Ok(())
}
fn repair_block_group_free_counts(
fs: &Filesystem,
group_index: u32,
stored_blocks: u32,
observed_blocks: u32,
stored_inodes: u32,
observed_inodes: u32,
report: &mut AuditReport,
) -> Result<()> {
let block_delta = (observed_blocks as i64) - (stored_blocks as i64);
let inode_delta = (observed_inodes as i64) - (stored_inodes as i64);
if block_delta == 0 && inode_delta == 0 {
return Ok(());
}
if block_delta < i32::MIN as i64
|| block_delta > i32::MAX as i64
|| inode_delta < i32::MIN as i64
|| inode_delta > i32::MAX as i64
{
return Ok(());
}
fs.patch_bgd_counters(
group_index as usize,
block_delta as i32,
inode_delta as i32,
0,
)?;
report.repaired_count += 1;
Ok(())
}
fn repair_superblock_free_counts(
fs: &Filesystem,
stored_blocks: u64,
observed_blocks: u64,
stored_inodes: u32,
observed_inodes: u32,
report: &mut AuditReport,
) -> Result<()> {
let block_delta = (observed_blocks as i64) - (stored_blocks as i64);
let inode_delta = (observed_inodes as i64) - (stored_inodes as i64);
if block_delta == 0 && inode_delta == 0 {
return Ok(());
}
if inode_delta < i32::MIN as i64 || inode_delta > i32::MAX as i64 {
return Ok(());
}
fs.patch_sb_counters(block_delta, inode_delta as i32)?;
report.repaired_count += 1;
Ok(())
}
fn finalize_and_commit_inode(
fs: &Filesystem,
ino: u32,
generation: u32,
raw: &mut [u8],
) -> Result<()> {
if fs.csum.enabled {
fs.csum.patch_inode_checksum(ino, generation, raw);
}
let mut buf = BlockBuffer::new(fs.sb.block_size());
fs.buffer_write_inode(&mut buf, ino, raw)?;
fs.commit_block_buffer(buf)
}
impl Filesystem {
pub fn audit(&self, max_dirs_visited: u32, max_entries_per_dir: u32) -> Result<AuditReport> {
audit(self, max_dirs_visited, max_entries_per_dir)
}
pub fn audit_repair(
&self,
max_dirs_visited: u32,
max_entries_per_dir: u32,
repair: bool,
) -> Result<AuditReport> {
audit_with_repair(
self,
max_dirs_visited,
max_entries_per_dir,
|_, _, _| {},
|_| {},
repair,
)
}
}