Skip to main content

btrfs_cli/rescue/
clear_ino_cache.rs

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    filesystem::Filesystem,
10    items,
11    path::BtrfsPath,
12    search::{self, SearchIntent},
13    transaction::Transaction,
14};
15use clap::Parser;
16use std::{
17    fs::OpenOptions,
18    io::{Read, Seek, Write},
19    path::PathBuf,
20};
21
22const ROOT_TREE_OBJECTID: u64 = 1;
23const FS_TREE_OBJECTID: u64 = raw::BTRFS_FS_TREE_OBJECTID as u64;
24const FIRST_FREE_OBJECTID: u64 = raw::BTRFS_FIRST_FREE_OBJECTID as u64;
25const LAST_FREE_OBJECTID: u64 =
26    (raw::BTRFS_LAST_FREE_OBJECTID as i64).cast_unsigned();
27const FREE_INO_OBJECTID: u64 =
28    (raw::BTRFS_FREE_INO_OBJECTID as i64).cast_unsigned();
29const FREE_SPACE_OBJECTID: u64 =
30    (raw::BTRFS_FREE_SPACE_OBJECTID as i64).cast_unsigned();
31
32/// True if `objectid` names a regular filesystem tree (the default
33/// subvolume or a user-created subvolume), as opposed to a
34/// system/internal tree.
35fn is_fs_tree(objectid: u64) -> bool {
36    objectid == FS_TREE_OBJECTID
37        || (FIRST_FREE_OBJECTID..=LAST_FREE_OBJECTID).contains(&objectid)
38}
39
40/// Remove leftover items pertaining to the deprecated inode cache feature
41///
42/// For every fs tree (the default subvolume and every user
43/// subvolume), walks the items keyed under
44/// `BTRFS_FREE_INO_OBJECTID` (and historically also
45/// `BTRFS_FREE_SPACE_OBJECTID`, which old kernels used for the
46/// per-inode cache bitmap), drops every referenced data extent via
47/// the delayed-ref queue, and deletes the items themselves. The
48/// transaction crate's data-ref drop path also trims the csum tree
49/// for any fully-freed extent.
50///
51/// The device must not be mounted.
52#[derive(Parser, Debug)]
53pub struct RescueClearInoCacheCommand {
54    /// Path to the btrfs device
55    device: PathBuf,
56}
57
58/// One item collected during the read pass.
59#[derive(Debug)]
60struct InoCacheItem {
61    key: DiskKey,
62    /// `Some` for `EXTENT_DATA` items that reference a regular extent
63    /// (and so need a data-ref drop in the apply pass). `None` for
64    /// every other item kind, which only needs deletion.
65    extent: Option<ExtentRef>,
66}
67
68#[derive(Debug)]
69struct ExtentRef {
70    disk_bytenr: u64,
71    disk_num_bytes: u64,
72}
73
74impl Runnable for RescueClearInoCacheCommand {
75    fn run(&self, _ctx: &RunContext) -> Result<()> {
76        if is_mounted(&self.device) {
77            bail!("{} is currently mounted", self.device.display());
78        }
79
80        let file = OpenOptions::new()
81            .read(true)
82            .write(true)
83            .open(&self.device)
84            .with_context(|| {
85                format!("failed to open '{}'", self.device.display())
86            })?;
87
88        let mut fs = Filesystem::open(file).with_context(|| {
89            format!("failed to open filesystem on '{}'", self.device.display())
90        })?;
91
92        // Pass 1: enumerate every fs tree and ensure each one is
93        // loaded into the in-memory roots map.
94        let fs_tree_ids = collect_fs_tree_ids(&mut fs)?;
95
96        // Pass 2: per fs tree, scan + apply within a single
97        // transaction so the data-ref drops and item deletions
98        // commit atomically.
99        let mut total_subvols = 0usize;
100        let mut total_items = 0usize;
101        let mut total_extents = 0usize;
102
103        for tree_id in fs_tree_ids {
104            // Skip subvolumes whose root is not loadable (could be
105            // an orphan ROOT_ITEM left by a half-deleted snapshot).
106            if fs.root_bytenr(tree_id).is_none() {
107                continue;
108            }
109
110            let items_to_clear = collect_ino_cache_items(&mut fs, tree_id)?;
111            if items_to_clear.is_empty() {
112                continue;
113            }
114
115            let mut trans = Transaction::start(&mut fs)
116                .context("failed to start transaction")?;
117
118            for item in &items_to_clear {
119                if let Some(ext) = &item.extent
120                    && ext.disk_bytenr != 0
121                {
122                    trans.delayed_refs.drop_data_ref(
123                        ext.disk_bytenr,
124                        ext.disk_num_bytes,
125                        tree_id,
126                        FREE_INO_OBJECTID,
127                        0,
128                        1,
129                    );
130                    total_extents += 1;
131                }
132
133                delete_one_item(&mut trans, &mut fs, tree_id, &item.key)?;
134            }
135
136            trans
137                .commit(&mut fs)
138                .context("failed to commit transaction")?;
139            fs.sync().context("failed to sync to disk")?;
140
141            total_subvols += 1;
142            total_items += items_to_clear.len();
143        }
144
145        if total_subvols == 0 {
146            println!("no inode cache items found on {}", self.device.display());
147        } else {
148            println!(
149                "cleared inode cache on {} ({} subvolume(s), {} item(s), {} data extent(s) freed)",
150                self.device.display(),
151                total_subvols,
152                total_items,
153                total_extents,
154            );
155        }
156        Ok(())
157    }
158}
159
160/// Walk the root tree and return every fs tree objectid.
161fn collect_fs_tree_ids<R: Read + Write + Seek>(
162    fs: &mut Filesystem<R>,
163) -> Result<Vec<u64>> {
164    let start = DiskKey {
165        objectid: 0,
166        key_type: KeyType::from_raw(0),
167        offset: 0,
168    };
169    let mut path = BtrfsPath::new();
170    search::search_slot(
171        None,
172        fs,
173        ROOT_TREE_OBJECTID,
174        &start,
175        &mut path,
176        SearchIntent::ReadOnly,
177        false,
178    )
179    .context("failed to walk root tree for ROOT_ITEMs")?;
180
181    let mut ids: Vec<u64> = Vec::new();
182    while let Some(leaf) = path.nodes[0].as_ref() {
183        let nritems = leaf.nritems() as usize;
184        if path.slots[0] >= nritems {
185            if !search::next_leaf(fs, &mut path).context("next_leaf failed")? {
186                break;
187            }
188            continue;
189        }
190        let key = leaf.item_key(path.slots[0]);
191        if key.key_type == KeyType::RootItem && is_fs_tree(key.objectid) {
192            // The same fs tree may have multiple ROOT_ITEMs at
193            // different offsets (snapshots). Only the canonical one
194            // gets registered in the roots map; uniquify here.
195            if ids.last().copied() != Some(key.objectid) {
196                ids.push(key.objectid);
197            }
198        }
199        path.slots[0] += 1;
200    }
201    path.release();
202    Ok(ids)
203}
204
205/// Collect every cache item to delete in `tree_id`. Returns items in
206/// the order they appear in the tree.
207fn collect_ino_cache_items<R: Read + Write + Seek>(
208    fs: &mut Filesystem<R>,
209    tree_id: u64,
210) -> Result<Vec<InoCacheItem>> {
211    let mut out: Vec<InoCacheItem> = Vec::new();
212    for objectid in [FREE_INO_OBJECTID, FREE_SPACE_OBJECTID] {
213        collect_for_objectid(fs, tree_id, objectid, &mut out)?;
214    }
215    Ok(out)
216}
217
218fn collect_for_objectid<R: Read + Write + Seek>(
219    fs: &mut Filesystem<R>,
220    tree_id: u64,
221    objectid: u64,
222    out: &mut Vec<InoCacheItem>,
223) -> Result<()> {
224    let start = DiskKey {
225        objectid,
226        key_type: KeyType::from_raw(0),
227        offset: 0,
228    };
229    let mut path = BtrfsPath::new();
230    search::search_slot(
231        None,
232        fs,
233        tree_id,
234        &start,
235        &mut path,
236        SearchIntent::ReadOnly,
237        false,
238    )
239    .with_context(|| {
240        format!("failed to search tree {tree_id} for objectid {objectid:#x}")
241    })?;
242
243    while let Some(leaf) = path.nodes[0].as_ref() {
244        let nritems = leaf.nritems() as usize;
245        if path.slots[0] >= nritems {
246            if !search::next_leaf(fs, &mut path).context("next_leaf failed")? {
247                break;
248            }
249            continue;
250        }
251        let key = leaf.item_key(path.slots[0]);
252        if key.objectid != objectid {
253            break;
254        }
255
256        let extent = if key.key_type == KeyType::ExtentData {
257            let data = leaf.item_data(path.slots[0]);
258            let fei = FileExtentItem::parse(data).with_context(|| {
259                format!(
260                    "failed to parse FILE_EXTENT for ino cache at offset {}",
261                    key.offset
262                )
263            })?;
264            match fei.body {
265                FileExtentBody::Regular {
266                    disk_bytenr,
267                    disk_num_bytes,
268                    ..
269                } => Some(ExtentRef {
270                    disk_bytenr,
271                    disk_num_bytes,
272                }),
273                FileExtentBody::Inline { .. } => None,
274            }
275        } else {
276            None
277        };
278
279        out.push(InoCacheItem { key, extent });
280        path.slots[0] += 1;
281    }
282    path.release();
283    Ok(())
284}
285
286/// Delete a single item identified by an exact key. Returns `false`
287/// (without erroring) if the item is missing.
288fn delete_one_item<R: Read + Write + Seek>(
289    trans: &mut Transaction<R>,
290    fs: &mut Filesystem<R>,
291    tree_id: u64,
292    key: &DiskKey,
293) -> Result<bool> {
294    let mut path = BtrfsPath::new();
295    let found = search::search_slot(
296        Some(trans),
297        fs,
298        tree_id,
299        key,
300        &mut path,
301        SearchIntent::Delete,
302        true,
303    )
304    .with_context(|| {
305        format!("failed to search for {key:?} in tree {tree_id}")
306    })?;
307    if !found {
308        path.release();
309        return Ok(false);
310    }
311    let leaf = path.nodes[0]
312        .as_mut()
313        .ok_or_else(|| anyhow::anyhow!("delete_one_item: no leaf in path"))?;
314    items::del_items(leaf, path.slots[0], 1);
315    fs.mark_dirty(leaf);
316    path.release();
317    Ok(true)
318}