use crate::{RunContext, Runnable, util::is_mounted};
use anyhow::{Context, Result, bail};
use btrfs_disk::{
raw,
superblock::ChecksumType,
tree::{DiskKey, KeyType},
util::btrfs_csum_data,
};
use btrfs_transaction::{
filesystem::Filesystem,
path::BtrfsPath,
search::{self, SearchIntent, next_leaf},
transaction::Transaction,
};
use clap::Parser;
use std::{
fs::OpenOptions,
io::{Read, Seek, Write},
path::PathBuf,
};
const CSUM_TREE_OBJECTID: u64 = raw::BTRFS_CSUM_TREE_OBJECTID as u64;
#[allow(clippy::cast_sign_loss)]
const EXTENT_CSUM_OBJECTID: u64 = raw::BTRFS_EXTENT_CSUM_OBJECTID as u64;
#[derive(Parser, Debug)]
pub struct RescueFixDataChecksumCommand {
device: PathBuf,
#[clap(short, long)]
readonly: bool,
#[clap(short, long)]
interactive: bool,
#[clap(short, long)]
mirror: Option<u32>,
}
struct Mismatch {
logical: u64,
new_csum: Vec<u8>,
}
impl Runnable for RescueFixDataChecksumCommand {
fn run(&self, _ctx: &RunContext) -> Result<()> {
if self.interactive {
bail!("--interactive mode is not yet implemented");
}
if let Some(m) = self.mirror
&& m != 1
{
bail!(
"only mirror 1 is supported (multi-device write paths are not implemented)"
);
}
let repair = !self.readonly && self.mirror.is_some();
if is_mounted(&self.device) {
bail!("{} is currently mounted", self.device.display());
}
let file = OpenOptions::new()
.read(true)
.write(true)
.open(&self.device)
.with_context(|| {
format!("failed to open '{}'", self.device.display())
})?;
let mut fs = Filesystem::open(file).with_context(|| {
format!("failed to open filesystem on '{}'", self.device.display())
})?;
let csum_type = fs.superblock.csum_type;
if !matches!(csum_type, ChecksumType::Crc32) {
bail!(
"unsupported csum type {csum_type:?}: only CRC32C is supported",
);
}
let csum_size = csum_type.size();
let sectorsize = u64::from(fs.superblock.sectorsize);
let mismatches = scan_csum_tree(&mut fs, csum_size, sectorsize)
.context("failed to scan csum tree")?;
if mismatches.is_empty() {
println!("no data checksum mismatch found");
return Ok(());
}
for m in &mismatches {
println!("logical={} csum mismatch", m.logical);
}
if !repair {
println!(
"{} mismatch(es) found; rerun with --mirror 1 to repair",
mismatches.len()
);
return Ok(());
}
apply_csum_updates(&mut fs, &mismatches, csum_size, sectorsize)
.context("failed to apply csum updates")?;
fs.sync().context("failed to sync to disk")?;
println!(
"{} csum item(s) updated using data from mirror 1",
mismatches.len()
);
Ok(())
}
}
fn scan_csum_tree<R: Read + Write + Seek>(
fs: &mut Filesystem<R>,
csum_size: usize,
sectorsize: u64,
) -> Result<Vec<Mismatch>> {
let start = DiskKey {
objectid: EXTENT_CSUM_OBJECTID,
key_type: KeyType::ExtentCsum,
offset: 0,
};
let mut path = BtrfsPath::new();
let _ = search::search_slot(
None,
fs,
CSUM_TREE_OBJECTID,
&start,
&mut path,
SearchIntent::ReadOnly,
false,
)?;
let mut entries: Vec<(u64, Vec<u8>)> = Vec::new();
'outer: loop {
{
let Some(leaf) = path.nodes[0].as_ref() else {
break;
};
let nritems = leaf.nritems() as usize;
while path.slots[0] < nritems {
let key = leaf.item_key(path.slots[0]);
if key.key_type != KeyType::ExtentCsum {
if key.objectid != EXTENT_CSUM_OBJECTID {
break 'outer;
}
path.slots[0] += 1;
continue;
}
let data = leaf.item_data(path.slots[0]);
let nsectors = data.len() / csum_size;
for i in 0..nsectors {
let logical = key.offset + (i as u64) * sectorsize;
let stored =
data[i * csum_size..(i + 1) * csum_size].to_vec();
entries.push((logical, stored));
}
path.slots[0] += 1;
}
}
if !next_leaf(fs, &mut path)? {
break;
}
}
path.release();
let mut mismatches = Vec::new();
let sector_usize = usize::try_from(sectorsize).unwrap();
for (logical, stored) in entries {
let buf = fs
.reader_mut()
.read_data(logical, sector_usize)
.with_context(|| {
format!("failed to read data at logical {logical}")
})?;
let computed = btrfs_csum_data(&buf).to_le_bytes();
if computed[..csum_size] != stored[..] {
mismatches.push(Mismatch {
logical,
new_csum: computed[..csum_size].to_vec(),
});
}
}
Ok(mismatches)
}
fn apply_csum_updates<R: Read + Write + Seek>(
fs: &mut Filesystem<R>,
mismatches: &[Mismatch],
csum_size: usize,
sectorsize: u64,
) -> Result<()> {
let mut trans =
Transaction::start(fs).context("failed to start transaction")?;
for m in mismatches {
let key = DiskKey {
objectid: EXTENT_CSUM_OBJECTID,
key_type: KeyType::ExtentCsum,
offset: m.logical,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut trans),
fs,
CSUM_TREE_OBJECTID,
&key,
&mut path,
SearchIntent::ReadOnly,
true,
)?;
if !found && path.slots[0] > 0 {
path.slots[0] -= 1;
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
anyhow::anyhow!(
"no leaf in path for csum update at logical {}",
m.logical
)
})?;
let item_key = leaf.item_key(path.slots[0]);
if item_key.key_type != KeyType::ExtentCsum {
bail!("no EXTENT_CSUM item containing logical {}", m.logical);
}
let item_size = leaf.item_size(path.slots[0]) as usize;
let nsectors = item_size / csum_size;
let item_first = item_key.offset;
let item_last_excl = item_first + (nsectors as u64) * sectorsize;
if m.logical < item_first || m.logical >= item_last_excl {
bail!(
"csum item at logical {} does not cover {}",
item_first,
m.logical
);
}
let sector_index =
usize::try_from((m.logical - item_first) / sectorsize).unwrap();
let off = sector_index * csum_size;
let data = leaf.item_data_mut(path.slots[0]);
data[off..off + csum_size].copy_from_slice(&m.new_csum);
fs.mark_dirty(leaf);
path.release();
}
trans.commit(fs).context("failed to commit transaction")?;
Ok(())
}