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 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 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
205fn 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
286fn 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}