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    loop {
183        let Some(leaf) = path.nodes[0].as_ref() else {
184            break;
185        };
186        let nritems = leaf.nritems() as usize;
187        if path.slots[0] >= nritems {
188            if !search::next_leaf(fs, &mut path).context("next_leaf failed")? {
189                break;
190            }
191            continue;
192        }
193        let key = leaf.item_key(path.slots[0]);
194        if key.key_type == KeyType::RootItem && is_fs_tree(key.objectid) {
195            // The same fs tree may have multiple ROOT_ITEMs at
196            // different offsets (snapshots). Only the canonical one
197            // gets registered in the roots map; uniquify here.
198            if ids.last().copied() != Some(key.objectid) {
199                ids.push(key.objectid);
200            }
201        }
202        path.slots[0] += 1;
203    }
204    path.release();
205    Ok(ids)
206}
207
208/// Collect every cache item to delete in `tree_id`. Returns items in
209/// the order they appear in the tree.
210fn collect_ino_cache_items<R: Read + Write + Seek>(
211    fs: &mut Filesystem<R>,
212    tree_id: u64,
213) -> Result<Vec<InoCacheItem>> {
214    let mut out: Vec<InoCacheItem> = Vec::new();
215    for objectid in [FREE_INO_OBJECTID, FREE_SPACE_OBJECTID] {
216        collect_for_objectid(fs, tree_id, objectid, &mut out)?;
217    }
218    Ok(out)
219}
220
221fn collect_for_objectid<R: Read + Write + Seek>(
222    fs: &mut Filesystem<R>,
223    tree_id: u64,
224    objectid: u64,
225    out: &mut Vec<InoCacheItem>,
226) -> Result<()> {
227    let start = DiskKey {
228        objectid,
229        key_type: KeyType::from_raw(0),
230        offset: 0,
231    };
232    let mut path = BtrfsPath::new();
233    search::search_slot(
234        None,
235        fs,
236        tree_id,
237        &start,
238        &mut path,
239        SearchIntent::ReadOnly,
240        false,
241    )
242    .with_context(|| {
243        format!("failed to search tree {tree_id} for objectid {objectid:#x}")
244    })?;
245
246    loop {
247        let Some(leaf) = path.nodes[0].as_ref() else {
248            break;
249        };
250        let nritems = leaf.nritems() as usize;
251        if path.slots[0] >= nritems {
252            if !search::next_leaf(fs, &mut path).context("next_leaf failed")? {
253                break;
254            }
255            continue;
256        }
257        let key = leaf.item_key(path.slots[0]);
258        if key.objectid != objectid {
259            break;
260        }
261
262        let extent = if key.key_type == KeyType::ExtentData {
263            let data = leaf.item_data(path.slots[0]);
264            let fei = FileExtentItem::parse(data).with_context(|| {
265                format!(
266                    "failed to parse FILE_EXTENT for ino cache at offset {}",
267                    key.offset
268                )
269            })?;
270            match fei.body {
271                FileExtentBody::Regular {
272                    disk_bytenr,
273                    disk_num_bytes,
274                    ..
275                } => Some(ExtentRef {
276                    disk_bytenr,
277                    disk_num_bytes,
278                }),
279                FileExtentBody::Inline { .. } => None,
280            }
281        } else {
282            None
283        };
284
285        out.push(InoCacheItem { key, extent });
286        path.slots[0] += 1;
287    }
288    path.release();
289    Ok(())
290}
291
292/// Delete a single item identified by an exact key. Returns `false`
293/// (without erroring) if the item is missing.
294fn delete_one_item<R: Read + Write + Seek>(
295    trans: &mut Transaction<R>,
296    fs: &mut Filesystem<R>,
297    tree_id: u64,
298    key: &DiskKey,
299) -> Result<bool> {
300    let mut path = BtrfsPath::new();
301    let found = search::search_slot(
302        Some(trans),
303        fs,
304        tree_id,
305        key,
306        &mut path,
307        SearchIntent::Delete,
308        true,
309    )
310    .with_context(|| {
311        format!("failed to search for {key:?} in tree {tree_id}")
312    })?;
313    if !found {
314        path.release();
315        return Ok(false);
316    }
317    let leaf = path.nodes[0]
318        .as_mut()
319        .ok_or_else(|| anyhow::anyhow!("delete_one_item: no leaf in path"))?;
320    items::del_items(leaf, path.slots[0], 1);
321    fs.mark_dirty(leaf);
322    path.release();
323    Ok(true)
324}