1use crate::{RunContext, Runnable, util::is_mounted};
2use anyhow::{Context, Result, bail};
3use btrfs_disk::{
4 items::{FileExtentBody, FileExtentItem},
5 raw,
6 tree::{DiskKey, KeyType},
7};
8use btrfs_transaction::{
9 allocation,
10 filesystem::Filesystem,
11 items,
12 path::BtrfsPath,
13 search::{self, SearchIntent},
14 transaction::Transaction,
15};
16use clap::Parser;
17use std::{
18 fs::OpenOptions,
19 io::{Read, Seek, Write},
20 path::PathBuf,
21};
22
23const FREE_SPACE_TREE_OBJECTID: u64 =
25 raw::BTRFS_FREE_SPACE_TREE_OBJECTID as u64;
26
27const ROOT_TREE_OBJECTID: u64 = 1;
29
30const FREE_SPACE_OBJECTID: u64 =
33 (raw::BTRFS_FREE_SPACE_OBJECTID as i64).cast_unsigned();
34
35const FREE_SPACE_HEADER_KEY_TYPE: u8 = 0;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
42pub enum SpaceCacheVersion {
43 V1,
44 V2,
45}
46
47#[derive(Parser, Debug)]
59pub struct RescueClearSpaceCacheCommand {
60 version: SpaceCacheVersion,
62
63 device: PathBuf,
65}
66
67fn collect_tree_blocks<R: Read + Write + Seek>(
70 fs: &mut Filesystem<R>,
71 bytenr: u64,
72 out: &mut Vec<(u64, u8)>,
73) -> Result<()> {
74 let eb = fs
75 .read_block(bytenr)
76 .with_context(|| format!("failed to read tree block at {bytenr}"))?;
77 let level = eb.level();
78 out.push((bytenr, level));
79
80 if eb.is_node() {
81 let nritems = eb.nritems() as usize;
82 for slot in 0..nritems {
83 let child = eb.key_ptr_blockptr(slot);
84 collect_tree_blocks(fs, child, out)?;
85 }
86 }
87 Ok(())
88}
89
90fn read_v1_cache_entry<R: Read + Write + Seek>(
94 fs: &mut Filesystem<R>,
95 bg_start: u64,
96) -> Result<Option<V1CacheEntry>> {
97 let header_key = DiskKey {
100 objectid: FREE_SPACE_OBJECTID,
101 key_type: KeyType::from_raw(FREE_SPACE_HEADER_KEY_TYPE),
102 offset: bg_start,
103 };
104 let mut path = BtrfsPath::new();
105 let found = search::search_slot(
106 None,
107 fs,
108 ROOT_TREE_OBJECTID,
109 &header_key,
110 &mut path,
111 SearchIntent::ReadOnly,
112 false,
113 )
114 .context("failed to search root tree for v1 cache header")?;
115 if !found {
116 path.release();
117 return Ok(None);
118 }
119 let leaf = path.nodes[0]
120 .as_ref()
121 .ok_or_else(|| anyhow::anyhow!("no leaf in v1 header path"))?;
122 let payload = leaf.item_data(path.slots[0]);
123 if payload.len() < 17 {
124 path.release();
125 bail!("FREE_SPACE_HEADER for bg {bg_start} truncated");
126 }
127 let ino = u64::from_le_bytes(payload[0..8].try_into().unwrap());
130 path.release();
131
132 let mut extents: Vec<V1Extent> = Vec::new();
136
137 let start = DiskKey {
138 objectid: ino,
139 key_type: KeyType::ExtentData,
140 offset: 0,
141 };
142 let mut path = BtrfsPath::new();
143 search::search_slot(
144 None,
145 fs,
146 ROOT_TREE_OBJECTID,
147 &start,
148 &mut path,
149 SearchIntent::ReadOnly,
150 false,
151 )
152 .context("failed to search root tree for v1 cache extents")?;
153
154 'walk: while let Some(leaf) = path.nodes[0].as_ref() {
155 let nritems = leaf.nritems() as usize;
156 if path.slots[0] >= nritems {
157 if !search::next_leaf(fs, &mut path).context("next_leaf failed")? {
158 break;
159 }
160 continue;
161 }
162 let key = leaf.item_key(path.slots[0]);
163 if key.objectid != ino {
164 break 'walk;
165 }
166 if key.key_type == KeyType::ExtentData {
167 let data = leaf.item_data(path.slots[0]);
168 let fei = FileExtentItem::parse(data).ok_or_else(|| {
169 anyhow::anyhow!(
170 "malformed FILE_EXTENT for v1 cache inode {ino} offset {}",
171 key.offset
172 )
173 })?;
174 match fei.body {
175 FileExtentBody::Regular {
176 disk_bytenr,
177 disk_num_bytes,
178 ..
179 } => {
180 extents.push(V1Extent {
181 file_offset: key.offset,
182 disk_bytenr,
183 disk_num_bytes,
184 });
185 }
186 FileExtentBody::Inline { .. } => {
187 extents.push(V1Extent {
192 file_offset: key.offset,
193 disk_bytenr: 0,
194 disk_num_bytes: 0,
195 });
196 }
197 }
198 }
199 path.slots[0] += 1;
200 }
201 path.release();
202
203 Ok(Some(V1CacheEntry { ino, extents }))
204}
205
206fn delete_one_item<R: Read + Write + Seek>(
210 trans: &mut Transaction<R>,
211 fs: &mut Filesystem<R>,
212 tree_id: u64,
213 key: &DiskKey,
214) -> Result<bool> {
215 let mut path = BtrfsPath::new();
216 let found = search::search_slot(
217 Some(trans),
218 fs,
219 tree_id,
220 key,
221 &mut path,
222 SearchIntent::Delete,
223 true,
224 )
225 .with_context(|| {
226 format!("failed to search for {key:?} in tree {tree_id}")
227 })?;
228 if !found {
229 path.release();
230 return Ok(false);
231 }
232 let leaf = path.nodes[0]
233 .as_mut()
234 .ok_or_else(|| anyhow::anyhow!("delete_one_item: no leaf in path"))?;
235 items::del_items(leaf, path.slots[0], 1);
236 fs.mark_dirty(leaf);
237 path.release();
238 Ok(true)
239}
240
241impl Runnable for RescueClearSpaceCacheCommand {
242 fn run(&self, _ctx: &RunContext) -> Result<()> {
243 if is_mounted(&self.device) {
244 bail!("{} is currently mounted", self.device.display());
245 }
246
247 match self.version {
248 SpaceCacheVersion::V1 => self.clear_v1(),
249 SpaceCacheVersion::V2 => self.clear_v2(),
250 }
251 }
252}
253
254struct V1CacheEntry {
258 ino: u64,
260 extents: Vec<V1Extent>,
262}
263
264struct V1Extent {
265 file_offset: u64,
267 disk_bytenr: u64,
269 disk_num_bytes: u64,
271}
272
273impl RescueClearSpaceCacheCommand {
274 fn clear_v1(&self) -> Result<()> {
285 let file = OpenOptions::new()
286 .read(true)
287 .write(true)
288 .open(&self.device)
289 .with_context(|| {
290 format!("failed to open '{}'", self.device.display())
291 })?;
292
293 let mut fs = Filesystem::open(file).with_context(|| {
294 format!("failed to open filesystem on '{}'", self.device.display())
295 })?;
296
297 let block_groups = allocation::load_block_groups(&mut fs)
300 .context("failed to load block groups")?;
301
302 let mut entries: Vec<(u64, V1CacheEntry)> = Vec::new();
303 for bg in &block_groups {
304 if let Some(entry) = read_v1_cache_entry(&mut fs, bg.start)? {
305 entries.push((bg.start, entry));
306 }
307 }
308
309 if entries.is_empty() {
310 if fs.superblock.cache_generation != u64::MAX {
314 let trans = Transaction::start(&mut fs)
315 .context("failed to start transaction")?;
316 fs.superblock.cache_generation = u64::MAX;
317 trans
318 .commit(&mut fs)
319 .context("failed to commit transaction")?;
320 fs.sync().context("failed to sync to disk")?;
321 }
322 println!(
323 "no v1 free space cache found on {}",
324 self.device.display()
325 );
326 return Ok(());
327 }
328
329 let mut trans = Transaction::start(&mut fs)
331 .context("failed to start transaction")?;
332
333 let mut total_extents_freed: usize = 0;
334 for (bg_start, entry) in &entries {
335 for ext in &entry.extents {
338 if ext.disk_bytenr == 0 {
339 continue; }
341 trans.delayed_refs.drop_data_ref(
342 ext.disk_bytenr,
343 ext.disk_num_bytes,
344 ROOT_TREE_OBJECTID,
345 entry.ino,
346 ext.file_offset,
347 1,
348 );
349 total_extents_freed += 1;
350 }
351
352 delete_one_item(
354 &mut trans,
355 &mut fs,
356 ROOT_TREE_OBJECTID,
357 &DiskKey {
358 objectid: FREE_SPACE_OBJECTID,
359 key_type: KeyType::from_raw(FREE_SPACE_HEADER_KEY_TYPE),
360 offset: *bg_start,
361 },
362 )?;
363
364 for ext in &entry.extents {
366 delete_one_item(
367 &mut trans,
368 &mut fs,
369 ROOT_TREE_OBJECTID,
370 &DiskKey {
371 objectid: entry.ino,
372 key_type: KeyType::ExtentData,
373 offset: ext.file_offset,
374 },
375 )?;
376 }
377
378 let _ = delete_one_item(
381 &mut trans,
382 &mut fs,
383 ROOT_TREE_OBJECTID,
384 &DiskKey {
385 objectid: entry.ino,
386 key_type: KeyType::InodeItem,
387 offset: 0,
388 },
389 );
390 }
391
392 fs.superblock.cache_generation = u64::MAX;
395
396 trans
397 .commit(&mut fs)
398 .context("failed to commit transaction")?;
399 fs.sync().context("failed to sync to disk")?;
400
401 println!(
402 "cleared v1 free space cache on {} ({} block group(s), {} data extent(s) freed)",
403 self.device.display(),
404 entries.len(),
405 total_extents_freed,
406 );
407 Ok(())
408 }
409
410 fn clear_v2(&self) -> Result<()> {
411 let file = OpenOptions::new()
412 .read(true)
413 .write(true)
414 .open(&self.device)
415 .with_context(|| {
416 format!("failed to open '{}'", self.device.display())
417 })?;
418
419 let mut fs = Filesystem::open(file).with_context(|| {
420 format!("failed to open filesystem on '{}'", self.device.display())
421 })?;
422
423 let fst_flag = u64::from(raw::BTRFS_FEATURE_COMPAT_RO_FREE_SPACE_TREE);
424 let fst_valid_flag =
425 u64::from(raw::BTRFS_FEATURE_COMPAT_RO_FREE_SPACE_TREE_VALID);
426 let bgt_flag = u64::from(raw::BTRFS_FEATURE_COMPAT_RO_BLOCK_GROUP_TREE);
427
428 if fs.superblock.compat_ro_flags & bgt_flag != 0 {
429 bail!(
430 "cannot clear free space tree: filesystem has block-group-tree \
431 enabled, which requires free-space-tree to mount"
432 );
433 }
434
435 if fs.superblock.compat_ro_flags & fst_flag == 0 {
436 println!("no free space tree to clear");
437 return Ok(());
438 }
439
440 let Some(fst_bytenr) = fs.root_bytenr(FREE_SPACE_TREE_OBJECTID) else {
441 fs.superblock.compat_ro_flags &= !(fst_flag | fst_valid_flag);
444 let trans = Transaction::start(&mut fs)
445 .context("failed to start transaction")?;
446 trans
447 .commit(&mut fs)
448 .context("failed to commit transaction")?;
449 fs.sync().context("failed to sync to disk")?;
450 println!("cleared free space tree compat_ro flags");
451 return Ok(());
452 };
453
454 let mut tree_blocks = Vec::new();
455 collect_tree_blocks(&mut fs, fst_bytenr, &mut tree_blocks)
456 .context("failed to walk free space tree")?;
457
458 fs.superblock.compat_ro_flags &= !(fst_flag | fst_valid_flag);
462
463 let mut trans = Transaction::start(&mut fs)
464 .context("failed to start transaction")?;
465
466 for &(bytenr, level) in &tree_blocks {
467 trans.delayed_refs.drop_ref(
468 bytenr,
469 true,
470 FREE_SPACE_TREE_OBJECTID,
471 level,
472 );
473 trans.pin_block(bytenr);
474 fs.evict_block(bytenr);
475 }
476
477 let root_key = DiskKey {
479 objectid: FREE_SPACE_TREE_OBJECTID,
480 key_type: KeyType::RootItem,
481 offset: 0,
482 };
483 let mut path = BtrfsPath::new();
484 let found = search::search_slot(
485 Some(&mut trans),
486 &mut fs,
487 ROOT_TREE_OBJECTID,
488 &root_key,
489 &mut path,
490 SearchIntent::Delete,
491 true,
492 )
493 .context("failed to search root tree for free space tree entry")?;
494
495 if found {
496 let leaf = path.nodes[0].as_mut().ok_or_else(|| {
497 anyhow::anyhow!("no leaf in path for root tree deletion")
498 })?;
499 items::del_items(leaf, path.slots[0], 1);
500 fs.mark_dirty(leaf);
501 }
502 path.release();
503
504 fs.remove_root(FREE_SPACE_TREE_OBJECTID);
508
509 trans
510 .commit(&mut fs)
511 .context("failed to commit transaction")?;
512 fs.sync().context("failed to sync to disk")?;
513
514 println!(
515 "cleared free space tree on {} ({} blocks freed), kernel will rebuild it on next mount",
516 self.device.display(),
517 tree_blocks.len()
518 );
519 Ok(())
520 }
521}