Skip to main content

btrfs_cli/inspect/
map_swapfile.rs

1use crate::{Format, Runnable};
2use anyhow::{Context, Result, bail};
3use btrfs_uapi::{
4    raw::{
5        BTRFS_BLOCK_GROUP_PROFILE_MASK, BTRFS_CHUNK_ITEM_KEY,
6        BTRFS_CHUNK_TREE_OBJECTID, BTRFS_EXTENT_DATA_KEY,
7        BTRFS_FIRST_CHUNK_TREE_OBJECTID, btrfs_chunk, btrfs_file_extent_item,
8        btrfs_stripe,
9    },
10    tree_search::{SearchKey, tree_search},
11    util::read_le_u64,
12};
13use clap::Parser;
14use std::{
15    fs::File,
16    mem,
17    os::unix::io::{AsFd, AsRawFd},
18    path::PathBuf,
19};
20
21/// Print physical offset of first block and resume offset if file is
22/// suitable as swapfile.
23///
24/// All conditions of swapfile extents are verified if they could pass
25/// kernel tests. Use the value of resume offset for
26/// /sys/power/resume_offset, this depends on the page size that is
27/// detected on this system.
28#[derive(Parser, Debug)]
29pub struct MapSwapfileCommand {
30    /// Print only the value of resume_offset
31    #[arg(short = 'r', long)]
32    resume_offset: bool,
33
34    /// Path to a file on the btrfs filesystem
35    path: PathBuf,
36}
37
38impl Runnable for MapSwapfileCommand {
39    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
40        let file = File::open(&self.path).with_context(|| {
41            format!("cannot open '{}'", self.path.display())
42        })?;
43
44        validate_file(&file, &self.path)?;
45
46        let fd = file.as_fd();
47        let chunks = read_chunk_tree(fd)?;
48
49        let tree_id = btrfs_uapi::inode::lookup_path_rootid(fd)
50            .context("cannot lookup parent subvolume")?;
51
52        let stat = nix::sys::stat::fstat(&file).context("cannot fstat file")?;
53
54        let physical_start =
55            map_physical_start(fd, tree_id, stat.st_ino, &chunks)?;
56
57        let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u64;
58        if self.resume_offset {
59            println!("{}", physical_start / page_size);
60        } else {
61            println!("Physical start: {:12}", physical_start);
62            println!("Resume offset:  {:12}", physical_start / page_size);
63        }
64
65        Ok(())
66    }
67}
68
69/// Validate that the file is on btrfs, is a regular file, is NOCOW,
70/// and is not compressed.
71fn validate_file(file: &File, path: &std::path::Path) -> Result<()> {
72    let stfs = nix::sys::statfs::fstatfs(file)
73        .with_context(|| format!("cannot statfs '{}'", path.display()))?;
74    if stfs.filesystem_type() != nix::sys::statfs::BTRFS_SUPER_MAGIC {
75        bail!("not a file on btrfs");
76    }
77
78    let stat = nix::sys::stat::fstat(file)
79        .with_context(|| format!("cannot fstat '{}'", path.display()))?;
80    if stat.st_mode & libc::S_IFMT != libc::S_IFREG {
81        bail!("not a regular file");
82    }
83
84    let mut flags: libc::c_long = 0;
85    let ret = unsafe {
86        libc::ioctl(file.as_raw_fd(), libc::FS_IOC_GETFLAGS, &mut flags)
87    };
88    if ret == -1 {
89        bail!(
90            "cannot verify file flags: {}",
91            std::io::Error::last_os_error()
92        );
93    }
94    const FS_NOCOW_FL: libc::c_long = 0x0080_0000;
95    const FS_COMPR_FL: libc::c_long = 0x0000_0004;
96    if flags & FS_NOCOW_FL == 0 {
97        bail!("file is not NOCOW");
98    }
99    if flags & FS_COMPR_FL != 0 {
100        bail!("file has COMPR attribute");
101    }
102
103    Ok(())
104}
105
106/// A parsed chunk from the chunk tree with stripe info.
107struct Chunk {
108    offset: u64,
109    length: u64,
110    stripe_len: u64,
111    type_flags: u64,
112    num_stripes: usize,
113    stripes: Vec<(u64, u64)>,
114}
115
116const CHUNK_LENGTH_OFF: usize = mem::offset_of!(btrfs_chunk, length);
117const CHUNK_STRIPE_LEN_OFF: usize = mem::offset_of!(btrfs_chunk, stripe_len);
118const CHUNK_TYPE_OFF: usize = mem::offset_of!(btrfs_chunk, type_);
119const CHUNK_NUM_STRIPES_OFF: usize = mem::offset_of!(btrfs_chunk, num_stripes);
120const CHUNK_FIRST_STRIPE_OFF: usize = mem::offset_of!(btrfs_chunk, stripe);
121const STRIPE_SIZE: usize = mem::size_of::<btrfs_stripe>();
122const STRIPE_DEVID_OFF: usize = mem::offset_of!(btrfs_stripe, devid);
123const STRIPE_OFFSET_OFF: usize = mem::offset_of!(btrfs_stripe, offset);
124
125fn read_le_u16(data: &[u8], off: usize) -> u16 {
126    u16::from_le_bytes(data[off..off + 2].try_into().unwrap())
127}
128
129/// Read all chunks from the chunk tree via tree search.
130fn read_chunk_tree(fd: std::os::unix::io::BorrowedFd) -> Result<Vec<Chunk>> {
131    let mut chunks = Vec::new();
132
133    tree_search(
134        fd,
135        SearchKey::for_objectid_range(
136            u64::from(BTRFS_CHUNK_TREE_OBJECTID),
137            BTRFS_CHUNK_ITEM_KEY,
138            u64::from(BTRFS_FIRST_CHUNK_TREE_OBJECTID),
139            u64::from(BTRFS_FIRST_CHUNK_TREE_OBJECTID),
140        ),
141        |hdr, data| {
142            let min_len = CHUNK_FIRST_STRIPE_OFF + STRIPE_SIZE;
143            if data.len() < min_len {
144                return Ok(());
145            }
146            let num_stripes = read_le_u16(data, CHUNK_NUM_STRIPES_OFF) as usize;
147            let expected_len =
148                CHUNK_FIRST_STRIPE_OFF + num_stripes * STRIPE_SIZE;
149            if data.len() < expected_len || num_stripes == 0 {
150                return Ok(());
151            }
152
153            let stripes = (0..num_stripes)
154                .map(|i| {
155                    let s = CHUNK_FIRST_STRIPE_OFF + i * STRIPE_SIZE;
156                    (
157                        read_le_u64(data, s + STRIPE_DEVID_OFF),
158                        read_le_u64(data, s + STRIPE_OFFSET_OFF),
159                    )
160                })
161                .collect();
162
163            chunks.push(Chunk {
164                offset: hdr.offset,
165                length: read_le_u64(data, CHUNK_LENGTH_OFF),
166                stripe_len: read_le_u64(data, CHUNK_STRIPE_LEN_OFF),
167                type_flags: read_le_u64(data, CHUNK_TYPE_OFF),
168                num_stripes,
169                stripes,
170            });
171            Ok(())
172        },
173    )
174    .context("failed to read chunk tree")?;
175
176    Ok(chunks)
177}
178
179/// Find the chunk containing `logical` via binary search.
180fn find_chunk(chunks: &[Chunk], logical: u64) -> Option<&Chunk> {
181    chunks
182        .binary_search_by(|c| {
183            if logical < c.offset {
184                std::cmp::Ordering::Greater
185            } else if logical >= c.offset + c.length {
186                std::cmp::Ordering::Less
187            } else {
188                std::cmp::Ordering::Equal
189            }
190        })
191        .ok()
192        .map(|i| &chunks[i])
193}
194
195/// A file extent parsed from the extent data tree search.
196struct FileExtent {
197    logical_offset: u64,
198    num_stripes: usize,
199    stripe_len: u64,
200    stripe_devid: u64,
201    stripe_physical: u64,
202    chunk_offset: u64,
203}
204
205const EXTENT_TYPE_OFF: usize = mem::offset_of!(btrfs_file_extent_item, type_);
206const EXTENT_COMPRESSION_OFF: usize =
207    mem::offset_of!(btrfs_file_extent_item, compression);
208const EXTENT_ENCRYPTION_OFF: usize =
209    mem::offset_of!(btrfs_file_extent_item, encryption);
210const EXTENT_OTHER_ENCODING_OFF: usize =
211    mem::offset_of!(btrfs_file_extent_item, other_encoding);
212const EXTENT_DISK_BYTENR_OFF: usize =
213    mem::offset_of!(btrfs_file_extent_item, disk_bytenr);
214
215/// Walk the extent data for a file and compute the physical start offset.
216fn map_physical_start(
217    fd: std::os::unix::io::BorrowedFd,
218    tree_id: u64,
219    ino: u64,
220    chunks: &[Chunk],
221) -> Result<u64> {
222    // Collect extents first, then validate (tree_search callback is nix::Result).
223    let mut extents: Vec<FileExtent> = Vec::new();
224    let mut error: Option<String> = None;
225
226    tree_search(
227        fd,
228        SearchKey {
229            tree_id,
230            min_objectid: ino,
231            max_objectid: ino,
232            min_type: BTRFS_EXTENT_DATA_KEY,
233            max_type: BTRFS_EXTENT_DATA_KEY,
234            min_offset: 0,
235            max_offset: u64::MAX,
236            min_transid: 0,
237            max_transid: u64::MAX,
238        },
239        |_hdr, data| {
240            if error.is_some() {
241                return Ok(());
242            }
243            if data.len() < mem::size_of::<btrfs_file_extent_item>() {
244                return Ok(());
245            }
246
247            let extent_type = data[EXTENT_TYPE_OFF];
248            // BTRFS_FILE_EXTENT_REG = 1, BTRFS_FILE_EXTENT_PREALLOC = 2
249            if extent_type != 1 && extent_type != 2 {
250                error = Some(if extent_type == 0 {
251                    "file with inline extent".to_string()
252                } else {
253                    format!("unknown extent type: {extent_type}")
254                });
255                return Ok(());
256            }
257
258            let logical_offset = read_le_u64(data, EXTENT_DISK_BYTENR_OFF);
259            if logical_offset == 0 {
260                error = Some("file with holes".to_string());
261                return Ok(());
262            }
263
264            if data[EXTENT_COMPRESSION_OFF] != 0 {
265                error = Some(format!(
266                    "compressed extent: {}",
267                    data[EXTENT_COMPRESSION_OFF]
268                ));
269                return Ok(());
270            }
271            if data[EXTENT_ENCRYPTION_OFF] != 0 {
272                error = Some(format!(
273                    "file with encryption: {}",
274                    data[EXTENT_ENCRYPTION_OFF]
275                ));
276                return Ok(());
277            }
278            let other_encoding = read_le_u16(data, EXTENT_OTHER_ENCODING_OFF);
279            if other_encoding != 0 {
280                error =
281                    Some(format!("file with other_encoding: {other_encoding}"));
282                return Ok(());
283            }
284
285            let chunk = match find_chunk(chunks, logical_offset) {
286                Some(c) => c,
287                None => {
288                    error = Some(format!(
289                        "cannot find chunk containing {logical_offset}"
290                    ));
291                    return Ok(());
292                }
293            };
294
295            if chunk.type_flags & u64::from(BTRFS_BLOCK_GROUP_PROFILE_MASK) != 0
296            {
297                error = Some(format!(
298                    "unsupported block group profile: {:#x}",
299                    chunk.type_flags
300                        & u64::from(BTRFS_BLOCK_GROUP_PROFILE_MASK)
301                ));
302                return Ok(());
303            }
304
305            extents.push(FileExtent {
306                logical_offset,
307                num_stripes: chunk.num_stripes,
308                stripe_len: chunk.stripe_len,
309                stripe_devid: chunk.stripes[0].0,
310                stripe_physical: chunk.stripes[0].1,
311                chunk_offset: chunk.offset,
312            });
313
314            Ok(())
315        },
316    )
317    .context("failed to search extent data")?;
318
319    if let Some(err) = error {
320        bail!("{err}");
321    }
322    if extents.is_empty() {
323        bail!("file has no extents");
324    }
325
326    // Validate all extents are on the same device.
327    let first_devid = extents[0].stripe_devid;
328    for ext in &extents[1..] {
329        if ext.stripe_devid != first_devid {
330            bail!("file stored on multiple devices");
331        }
332    }
333
334    // Compute physical offset from the first extent.
335    let ext = &extents[0];
336    // For single profile (validated above), num_stripes == 1 and stripe_index
337    // is always 0. The general formula from the C reference simplifies to:
338    let offset = ext.logical_offset - ext.chunk_offset;
339    let stripe_nr = offset / ext.stripe_len;
340    let stripe_offset = offset - stripe_nr * ext.stripe_len;
341    let physical_start = ext.stripe_physical
342        + (stripe_nr / ext.num_stripes as u64) * ext.stripe_len
343        + stripe_offset;
344
345    Ok(physical_start)
346}