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
32fn is_fs_tree(objectid: u64) -> bool {
36 objectid == FS_TREE_OBJECTID
37 || (FIRST_FREE_OBJECTID..=LAST_FREE_OBJECTID).contains(&objectid)
38}
39
40#[derive(Parser, Debug)]
53pub struct RescueClearInoCacheCommand {
54 device: PathBuf,
56}
57
58#[derive(Debug)]
60struct InoCacheItem {
61 key: DiskKey,
62 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 let fs_tree_ids = collect_fs_tree_ids(&mut fs)?;
95
96 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 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
160fn 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 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
208fn 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
292fn 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}