Skip to main content

btrfs_cli/rescue/
fix_data_checksum.rs

1use crate::{RunContext, Runnable, util::is_mounted};
2use anyhow::{Context, Result, bail};
3use btrfs_disk::{
4    raw,
5    superblock::ChecksumType,
6    tree::{DiskKey, KeyType},
7    util::btrfs_csum_data,
8};
9use btrfs_transaction::{
10    filesystem::Filesystem,
11    path::BtrfsPath,
12    search::{self, SearchIntent, next_leaf},
13    transaction::Transaction,
14};
15use clap::Parser;
16use std::{
17    fs::OpenOptions,
18    io::{Read, Seek, Write},
19    path::PathBuf,
20};
21
22/// Csum tree id.
23const CSUM_TREE_OBJECTID: u64 = raw::BTRFS_CSUM_TREE_OBJECTID as u64;
24/// Special objectid that holds EXTENT_CSUM keys in the csum tree
25/// (binds as `i32 = -10`; the kernel treats it as `-10ULL`, i.e.
26/// `0xFFFFFFFF_FFFFFFF6`).
27#[allow(clippy::cast_sign_loss)]
28const EXTENT_CSUM_OBJECTID: u64 = raw::BTRFS_EXTENT_CSUM_OBJECTID as u64;
29
30/// Fix data checksum mismatches
31///
32/// Walks the csum tree, recomputes the CRC32C of every covered data
33/// block, and reports any mismatches. With `--mirror N`, mismatched
34/// csums are rewritten in place using the data from mirror N (only
35/// mirror 1 is currently supported, since multi-device write paths
36/// are not yet implemented).
37///
38/// The device must not be mounted.
39#[derive(Parser, Debug)]
40pub struct RescueFixDataChecksumCommand {
41    /// Device to operate on
42    device: PathBuf,
43
44    /// Readonly mode, only report errors without repair
45    #[clap(short, long)]
46    readonly: bool,
47
48    /// Interactive mode, ignore the error by default
49    #[clap(short, long)]
50    interactive: bool,
51
52    /// Update csum item using specified mirror
53    #[clap(short, long)]
54    mirror: Option<u32>,
55}
56
57/// One detected csum mismatch.
58struct Mismatch {
59    /// Logical address of the mismatching sector.
60    logical: u64,
61    /// New (computed) checksum bytes that should replace the stored one.
62    new_csum: Vec<u8>,
63}
64
65impl Runnable for RescueFixDataChecksumCommand {
66    fn run(&self, _ctx: &RunContext) -> Result<()> {
67        if self.interactive {
68            bail!("--interactive mode is not yet implemented");
69        }
70        if let Some(m) = self.mirror
71            && m != 1
72        {
73            bail!(
74                "only mirror 1 is supported (multi-device write paths are not implemented)"
75            );
76        }
77        let repair = !self.readonly && self.mirror.is_some();
78
79        if is_mounted(&self.device) {
80            bail!("{} is currently mounted", self.device.display());
81        }
82
83        let file = OpenOptions::new()
84            .read(true)
85            .write(true)
86            .open(&self.device)
87            .with_context(|| {
88                format!("failed to open '{}'", self.device.display())
89            })?;
90
91        let mut fs = Filesystem::open(file).with_context(|| {
92            format!("failed to open filesystem on '{}'", self.device.display())
93        })?;
94
95        let csum_type = fs.superblock.csum_type;
96        if !matches!(csum_type, ChecksumType::Crc32) {
97            bail!(
98                "unsupported csum type {csum_type:?}: only CRC32C is supported",
99            );
100        }
101        let csum_size = csum_type.size();
102        let sectorsize = u64::from(fs.superblock.sectorsize);
103
104        let mismatches = scan_csum_tree(&mut fs, csum_size, sectorsize)
105            .context("failed to scan csum tree")?;
106
107        if mismatches.is_empty() {
108            println!("no data checksum mismatch found");
109            return Ok(());
110        }
111
112        for m in &mismatches {
113            println!("logical={} csum mismatch", m.logical);
114        }
115
116        if !repair {
117            println!(
118                "{} mismatch(es) found; rerun with --mirror 1 to repair",
119                mismatches.len()
120            );
121            return Ok(());
122        }
123
124        apply_csum_updates(&mut fs, &mismatches, csum_size, sectorsize)
125            .context("failed to apply csum updates")?;
126        fs.sync().context("failed to sync to disk")?;
127
128        println!(
129            "{} csum item(s) updated using data from mirror 1",
130            mismatches.len()
131        );
132        Ok(())
133    }
134}
135
136/// Walk the csum tree, verifying every per-sector csum and collecting
137/// mismatches.
138fn scan_csum_tree<R: Read + Write + Seek>(
139    fs: &mut Filesystem<R>,
140    csum_size: usize,
141    sectorsize: u64,
142) -> Result<Vec<Mismatch>> {
143    let start = DiskKey {
144        objectid: EXTENT_CSUM_OBJECTID,
145        key_type: KeyType::ExtentCsum,
146        offset: 0,
147    };
148    let mut path = BtrfsPath::new();
149    let _ = search::search_slot(
150        None,
151        fs,
152        CSUM_TREE_OBJECTID,
153        &start,
154        &mut path,
155        SearchIntent::ReadOnly,
156        false,
157    )?;
158
159    // Collect (logical, stored_csum_bytes) pairs first so we don't
160    // hold a borrow on the path while reading data.
161    let mut entries: Vec<(u64, Vec<u8>)> = Vec::new();
162    'outer: loop {
163        {
164            let Some(leaf) = path.nodes[0].as_ref() else {
165                break;
166            };
167            let nritems = leaf.nritems() as usize;
168            while path.slots[0] < nritems {
169                let key = leaf.item_key(path.slots[0]);
170                if key.key_type != KeyType::ExtentCsum {
171                    if key.objectid != EXTENT_CSUM_OBJECTID {
172                        break 'outer;
173                    }
174                    path.slots[0] += 1;
175                    continue;
176                }
177                let data = leaf.item_data(path.slots[0]);
178                // item_size = csum_size * (covered_sectors)
179                let nsectors = data.len() / csum_size;
180                for i in 0..nsectors {
181                    let logical = key.offset + (i as u64) * sectorsize;
182                    let stored =
183                        data[i * csum_size..(i + 1) * csum_size].to_vec();
184                    entries.push((logical, stored));
185                }
186                path.slots[0] += 1;
187            }
188        }
189        if !next_leaf(fs, &mut path)? {
190            break;
191        }
192    }
193    path.release();
194
195    let mut mismatches = Vec::new();
196    let sector_usize = usize::try_from(sectorsize).unwrap();
197    for (logical, stored) in entries {
198        let buf = fs
199            .reader_mut()
200            .read_data(logical, sector_usize)
201            .with_context(|| {
202                format!("failed to read data at logical {logical}")
203            })?;
204        let computed = btrfs_csum_data(&buf).to_le_bytes();
205        if computed[..csum_size] != stored[..] {
206            mismatches.push(Mismatch {
207                logical,
208                new_csum: computed[..csum_size].to_vec(),
209            });
210        }
211    }
212
213    Ok(mismatches)
214}
215
216/// Apply csum updates to the csum tree. For each mismatch, search the
217/// csum tree for the item containing the logical address, locate the
218/// per-sector slot inside it, and rewrite the bytes in place.
219fn apply_csum_updates<R: Read + Write + Seek>(
220    fs: &mut Filesystem<R>,
221    mismatches: &[Mismatch],
222    csum_size: usize,
223    sectorsize: u64,
224) -> Result<()> {
225    let mut trans =
226        Transaction::start(fs).context("failed to start transaction")?;
227    for m in mismatches {
228        let key = DiskKey {
229            objectid: EXTENT_CSUM_OBJECTID,
230            key_type: KeyType::ExtentCsum,
231            offset: m.logical,
232        };
233        let mut path = BtrfsPath::new();
234        let found = search::search_slot(
235            Some(&mut trans),
236            fs,
237            CSUM_TREE_OBJECTID,
238            &key,
239            &mut path,
240            SearchIntent::ReadOnly,
241            true,
242        )?;
243
244        // The csum item key.offset is the logical address of the
245        // *first* sector in the item; if our exact key wasn't found,
246        // step back one slot to land on the containing item.
247        if !found && path.slots[0] > 0 {
248            path.slots[0] -= 1;
249        }
250
251        let leaf = path.nodes[0].as_mut().ok_or_else(|| {
252            anyhow::anyhow!(
253                "no leaf in path for csum update at logical {}",
254                m.logical
255            )
256        })?;
257        let item_key = leaf.item_key(path.slots[0]);
258        if item_key.key_type != KeyType::ExtentCsum {
259            bail!("no EXTENT_CSUM item containing logical {}", m.logical);
260        }
261        let item_size = leaf.item_size(path.slots[0]) as usize;
262        let nsectors = item_size / csum_size;
263        let item_first = item_key.offset;
264        let item_last_excl = item_first + (nsectors as u64) * sectorsize;
265        if m.logical < item_first || m.logical >= item_last_excl {
266            bail!(
267                "csum item at logical {} does not cover {}",
268                item_first,
269                m.logical
270            );
271        }
272        let sector_index =
273            usize::try_from((m.logical - item_first) / sectorsize).unwrap();
274        let off = sector_index * csum_size;
275        let data = leaf.item_data_mut(path.slots[0]);
276        data[off..off + csum_size].copy_from_slice(&m.new_csum);
277        fs.mark_dirty(leaf);
278        path.release();
279    }
280    trans.commit(fs).context("failed to commit transaction")?;
281    Ok(())
282}