btrfs_cli/rescue/
fix_data_checksum.rs1use 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
22const CSUM_TREE_OBJECTID: u64 = raw::BTRFS_CSUM_TREE_OBJECTID as u64;
24#[allow(clippy::cast_sign_loss)]
28const EXTENT_CSUM_OBJECTID: u64 = raw::BTRFS_EXTENT_CSUM_OBJECTID as u64;
29
30#[derive(Parser, Debug)]
40pub struct RescueFixDataChecksumCommand {
41 device: PathBuf,
43
44 #[clap(short, long)]
46 readonly: bool,
47
48 #[clap(short, long)]
50 interactive: bool,
51
52 #[clap(short, long)]
54 mirror: Option<u32>,
55}
56
57struct Mismatch {
59 logical: u64,
61 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
136fn 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 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 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
216fn 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 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}