Skip to main content

btrfs_core/
fstree.rs

1//! P2 root-tree navigation + FS-tree inode / directory / path resolution.
2//!
3//! The superblock's `root` logical address names the **ROOT_TREE**, which holds
4//! a `ROOT_ITEM` (key type [`ROOT_ITEM_KEY`]) per tree; the one keyed by
5//! objectid [`FS_TREE_OBJECTID`] (5) carries the FS_TREE's own root `bytenr`
6//! (logical) and `level`. The FS_TREE then holds, per inode, an `INODE_ITEM`
7//! ([`INODE_ITEM_KEY`], the metadata), `INODE_REF`s ([`INODE_REF_KEY`],
8//! name↔parent links), and `DIR_ITEM`/`DIR_INDEX` entries ([`DIR_ITEM_KEY`] /
9//! [`DIR_INDEX_KEY`], directory children).
10//!
11//! Struct layouts (`btrfs_root_item`, `btrfs_inode_item`, `btrfs_timespec`,
12//! `btrfs_inode_ref`, `btrfs_dir_item`) were transcribed from the on-disk ABI
13//! and **verified byte-for-byte against `btrfs inspect-internal dump-tree`**
14//! plus an independent mount-ro `stat` on the minted oracle (see
15//! `tests/data/README.md`, "P2 ground truth").
16//!
17//! # Scope
18//!
19//! P2 navigates within the FS_TREE **leaf** the oracle presents (a single
20//! `nodesize` node). The public `read_inode` / `list_dir` / `read_by_path`
21//! entry points take a parsed [`Node`] so they work on the committed fixture and
22//! on any FS_TREE leaf read via `read_node`. Multi-leaf FS trees (interior
23//! descent within the FS tree) arrive with a later phase; here every inode /
24//! directory of the oracle lives in one leaf, matching dump-tree.
25//!
26//! # Safety
27//!
28//! This parses untrusted, attacker-controllable images. Every field is read
29//! through the bounds-checked [`crate::bytes`] helpers, and every `name_len` /
30//! `data_len` is clamped to the item data actually present, so a lying length
31//! yields a truncated name rather than an over-read or panic (the Paranoid
32//! Gatekeeper standard).
33
34use crate::bytes::{le_u16, le_u32, le_u64, u8_at};
35use crate::error::BtrfsError;
36use crate::node::{read_node, ChunkMap, Node};
37use crate::superblock::Superblock;
38
39/// `BTRFS_FS_TREE_OBJECTID` — the default subvolume / FS tree objectid (5).
40pub const FS_TREE_OBJECTID: u64 = 5;
41
42/// `BTRFS_FIRST_FREE_OBJECTID` — the FS_TREE's root directory inode (256); the
43/// first normal (non-reserved) objectid, where path resolution begins.
44pub const FS_TREE_ROOT_DIR_OBJECTID: u64 = 256;
45
46/// `BTRFS_INODE_ITEM_KEY` — a `btrfs_inode_item` (inode metadata).
47pub const INODE_ITEM_KEY: u8 = 1;
48
49/// `BTRFS_INODE_REF_KEY` — a `btrfs_inode_ref` (name + parent-dir link).
50pub const INODE_REF_KEY: u8 = 12;
51
52/// `BTRFS_DIR_ITEM_KEY` — a `btrfs_dir_item` keyed by name hash.
53pub const DIR_ITEM_KEY: u8 = 84;
54
55/// `BTRFS_DIR_INDEX_KEY` — a `btrfs_dir_item` keyed by sequential index.
56pub const DIR_INDEX_KEY: u8 = 96;
57
58/// `BTRFS_ROOT_ITEM_KEY` — a `btrfs_root_item` in the root tree (132).
59pub const ROOT_ITEM_KEY: u8 = 132;
60
61/// `sizeof(btrfs_inode_item)` on disk = 160 bytes: the scalar block (up to
62/// `reserved[4]` = 112 bytes) + four 12-byte [`Timestamp`]s.
63pub const INODE_ITEM_SIZE: usize = 160;
64
65/// `sizeof(btrfs_timespec)` on disk: `sec(u64) + nsec(u32)` = 12 bytes.
66pub const TIMESTAMP_SIZE: usize = 12;
67
68/// The `btrfs_inode_item.size` (logical file size) field offset — exposed so the
69/// EXTENT_DATA reader can truncate assembled content to the inode's size without
70/// re-decoding the whole inode.
71pub(crate) const INODE_SIZE_OFFSET: usize = 16;
72
73// `btrfs_inode_item` field offsets (verified vs dump-tree + stat).
74mod inode_off {
75    pub const GENERATION: usize = 0;
76    pub const TRANSID: usize = 8;
77    pub const SIZE: usize = 16;
78    pub const NBYTES: usize = 24;
79    pub const NLINK: usize = 40;
80    pub const UID: usize = 44;
81    pub const GID: usize = 48;
82    pub const MODE: usize = 52;
83    pub const FLAGS: usize = 64;
84    // The four timestamps begin after the 112-byte scalar block.
85    pub const ATIME: usize = 112;
86    pub const CTIME: usize = 124;
87    pub const MTIME: usize = 136;
88    pub const OTIME: usize = 148;
89}
90
91// `btrfs_root_item` field offsets (the leading 160 bytes are an embedded
92// `btrfs_inode_item`, so the root-item scalars start at 160).
93mod root_off {
94    pub const GENERATION: usize = 160;
95    pub const ROOT_DIRID: usize = 168;
96    pub const BYTENR: usize = 176;
97    pub const LEVEL: usize = 238;
98}
99
100// `btrfs_dir_item` field offsets: a 17-byte location key, then `transid(u64)`,
101// `data_len(u16)@25`, `name_len(u16)@27`, `type(u8)@29`, `name[]@30`.
102mod dir_off {
103    pub const NAME_LEN: usize = 27;
104    pub const TYPE: usize = 29;
105    pub const NAME: usize = 30;
106}
107
108/// A `btrfs_timespec`: seconds since the Unix epoch + nanoseconds.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
110pub struct Timestamp {
111    /// `sec` — signed seconds since 1970-01-01 (stored as `u64` on disk; btrfs
112    /// timestamps are post-epoch on real images).
113    pub sec: u64,
114    /// `nsec` — the nanosecond fraction (`0..1_000_000_000`).
115    pub nsec: u32,
116}
117
118impl Timestamp {
119    /// Decode a 12-byte `btrfs_timespec` at `off` within `data`
120    /// (bounds-checked; out-of-range fields read as 0).
121    #[must_use]
122    fn parse(data: &[u8], off: usize) -> Self {
123        Timestamp {
124            sec: le_u64(data, off),
125            nsec: le_u32(data, off + 8),
126        }
127    }
128}
129
130/// A decoded `btrfs_inode_item`: file/dir metadata + the four timestamps.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132#[non_exhaustive]
133pub struct Inode {
134    /// The inode's own objectid (the FS_TREE key objectid it was found under).
135    pub objectid: u64,
136    /// `generation` — the transaction id that created the inode.
137    pub generation: u64,
138    /// `transid` — the transaction id of the last metadata change.
139    pub transid: u64,
140    /// `size` — the logical file size in bytes (directory entry-name bytes for a
141    /// directory).
142    pub size: u64,
143    /// `nbytes` — bytes actually allocated on disk (0 for a hole-only / empty).
144    pub nbytes: u64,
145    /// `nlink` — the hard-link count.
146    pub nlink: u32,
147    /// `uid` — owner user id.
148    pub uid: u32,
149    /// `gid` — owner group id.
150    pub gid: u32,
151    /// `mode` — the POSIX mode bits, including the file-type bits (e.g.
152    /// `0o100644` for a regular file, `0o40755` for a directory).
153    pub mode: u32,
154    /// `flags` — the `BTRFS_INODE_*` flag bits.
155    pub flags: u64,
156    /// `atime` — last access time.
157    pub atime: Timestamp,
158    /// `ctime` — last status-change (inode) time.
159    pub ctime: Timestamp,
160    /// `mtime` — last data-modification time.
161    pub mtime: Timestamp,
162    /// `otime` — creation (birth) time.
163    pub otime: Timestamp,
164}
165
166/// The POSIX file-type mask (`S_IFMT`): the high bits of `mode` selecting the
167/// object kind.
168const S_IFMT: u32 = 0o170_000;
169/// `S_IFDIR` — the mode bits marking a directory.
170const S_IFDIR: u32 = 0o40_000;
171/// `S_IFREG` — the mode bits marking a regular file.
172const S_IFREG: u32 = 0o100_000;
173
174impl Inode {
175    /// Decode a `btrfs_inode_item` (`data`) for objectid `objectid`. All four
176    /// timestamps are decoded; out-of-range fields read as 0 (bounds-checked).
177    #[must_use]
178    fn parse(objectid: u64, data: &[u8]) -> Self {
179        Inode {
180            objectid,
181            generation: le_u64(data, inode_off::GENERATION),
182            transid: le_u64(data, inode_off::TRANSID),
183            size: le_u64(data, inode_off::SIZE),
184            nbytes: le_u64(data, inode_off::NBYTES),
185            nlink: le_u32(data, inode_off::NLINK),
186            uid: le_u32(data, inode_off::UID),
187            gid: le_u32(data, inode_off::GID),
188            mode: le_u32(data, inode_off::MODE),
189            flags: le_u64(data, inode_off::FLAGS),
190            atime: Timestamp::parse(data, inode_off::ATIME),
191            ctime: Timestamp::parse(data, inode_off::CTIME),
192            mtime: Timestamp::parse(data, inode_off::MTIME),
193            otime: Timestamp::parse(data, inode_off::OTIME),
194        }
195    }
196
197    /// `true` if the mode's type bits mark a directory.
198    #[must_use]
199    pub fn is_dir(&self) -> bool {
200        self.mode & S_IFMT == S_IFDIR
201    }
202
203    /// `true` if the mode's type bits mark a regular file.
204    #[must_use]
205    pub fn is_file(&self) -> bool {
206        self.mode & S_IFMT == S_IFREG
207    }
208}
209
210/// The `btrfs_dir_item` `type` byte — the kind of the child a directory entry
211/// points at. Only the members the oracle exercises are named; any other value
212/// is preserved in [`DirItemType::Other`] with its raw byte (fail-loud: the
213/// unknown value is shown, never dropped).
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215#[non_exhaustive]
216pub enum DirItemType {
217    /// A regular file (`BTRFS_FT_REG_FILE` = 1).
218    File,
219    /// A directory (`BTRFS_FT_DIR` = 2).
220    Dir,
221    /// A symbolic link (`BTRFS_FT_SYMLINK` = 7).
222    Symlink,
223    /// Any other `BTRFS_FT_*` value, carrying the raw byte so an "unknown type"
224    /// is identifiable rather than silently dropped.
225    Other(u8),
226}
227
228impl DirItemType {
229    /// Classify a raw `btrfs_dir_item` `type` byte.
230    #[must_use]
231    fn from_byte(b: u8) -> Self {
232        match b {
233            1 => DirItemType::File,
234            2 => DirItemType::Dir,
235            7 => DirItemType::Symlink,
236            other => DirItemType::Other(other),
237        }
238    }
239}
240
241/// One directory entry: a child's name, its inode objectid, and its declared
242/// type.
243#[derive(Debug, Clone, PartialEq, Eq)]
244#[non_exhaustive]
245pub struct DirEntry {
246    /// The entry name (UTF-8 lossy; a directory entry name is raw bytes on disk,
247    /// decoded permissively so a non-UTF-8 name is surfaced, not dropped).
248    pub name: String,
249    /// The child inode's objectid (`location.objectid` of the dir item).
250    pub child: u64,
251    /// The child's declared type.
252    pub item_type: DirItemType,
253}
254
255/// The FS_TREE root as located in the root tree: where the FS tree's own root
256/// node lives and how deep it is.
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258#[non_exhaustive]
259pub struct FsTreeRoot {
260    /// `bytenr` — the FS_TREE root node's **logical** address.
261    pub bytenr: u64,
262    /// `level` — the FS_TREE root node's B-tree level (0 = a single leaf).
263    pub level: u8,
264    /// `root_dirid` — the FS tree's root directory objectid (256).
265    pub root_dirid: u64,
266    /// `generation` — the transaction id the FS tree root was written in.
267    pub generation: u64,
268}
269
270/// Locate the FS_TREE ([`FS_TREE_OBJECTID`]) `ROOT_ITEM` by walking the root
271/// tree from the superblock's `root` logical address, returning where the FS
272/// tree's own root node lives.
273///
274/// Reads the root-tree node via [`read_node`]; if it is a leaf, scans its
275/// `ROOT_ITEM`s for the FS_TREE entry. (The oracle's root tree is a single
276/// leaf; interior root-tree descent is a later phase — an FS_TREE ROOT_ITEM in
277/// an interior root tree would require following key-pointers, out of P2 scope.)
278///
279/// # Errors
280///
281/// - Any error from [`read_node`] translating/reading `sb.root`.
282/// - [`BtrfsError::Truncated`] naming `FS_TREE ROOT_ITEM` when the root-tree
283///   leaf carries no FS_TREE `ROOT_ITEM` (a loud miss, never a silent `None`).
284pub fn fs_tree_root(
285    image: &[u8],
286    sb: &Superblock,
287    chunk_map: &ChunkMap,
288) -> Result<FsTreeRoot, BtrfsError> {
289    let node = read_node(image, sb, chunk_map, sb.root)?;
290    for (key, data) in node.leaf_items() {
291        if key.objectid == FS_TREE_OBJECTID && key.key_type == ROOT_ITEM_KEY {
292            return Ok(FsTreeRoot {
293                bytenr: le_u64(data, root_off::BYTENR),
294                level: u8_at(data, root_off::LEVEL),
295                root_dirid: le_u64(data, root_off::ROOT_DIRID),
296                generation: le_u64(data, root_off::GENERATION),
297            });
298        }
299    }
300    Err(BtrfsError::Truncated {
301        structure: "FS_TREE ROOT_ITEM (objectid 5) in root tree",
302        need: FS_TREE_OBJECTID as usize,
303        have: node.header.nritems as usize,
304    })
305}
306
307/// Read the `INODE_ITEM` for `objectid` from an FS_TREE `leaf`, or `None` if the
308/// leaf holds no inode item for it.
309#[must_use]
310pub fn read_inode(leaf: &Node, objectid: u64) -> Option<Inode> {
311    leaf.leaf_items()
312        .find(|(key, _)| key.objectid == objectid && key.key_type == INODE_ITEM_KEY)
313        .map(|(_, data)| Inode::parse(objectid, data))
314}
315
316/// List a directory's entries from an FS_TREE `leaf`: every `DIR_ITEM` /
317/// `DIR_INDEX` keyed by `dir_objectid`, deduplicated by name (both a `DIR_ITEM`
318/// and a `DIR_INDEX` name each child; the listing surfaces each name once).
319///
320/// Bounds-checked: an out-of-range `name_len` yields a name clamped to the item
321/// data actually present, never an over-read.
322#[must_use]
323pub fn list_dir(leaf: &Node, dir_objectid: u64) -> Vec<DirEntry> {
324    let mut out: Vec<DirEntry> = Vec::new();
325    for (key, data) in leaf.leaf_items() {
326        if key.objectid != dir_objectid
327            || (key.key_type != DIR_ITEM_KEY && key.key_type != DIR_INDEX_KEY)
328        {
329            continue;
330        }
331        let Some(entry) = parse_dir_item(data) else {
332            continue;
333        };
334        if out
335            .iter()
336            .any(|e| e.name == entry.name && e.child == entry.child)
337        {
338            continue;
339        }
340        out.push(entry);
341    }
342    out
343}
344
345/// Decode a single `btrfs_dir_item` body into a [`DirEntry`], or `None` if the
346/// item is too short to hold even the fixed prefix.
347///
348/// The name is clamped to whatever bytes remain after the fixed prefix, so a
349/// lying `name_len` truncates rather than over-reads.
350#[must_use]
351fn parse_dir_item(data: &[u8]) -> Option<DirEntry> {
352    if data.len() < dir_off::NAME {
353        return None;
354    }
355    let child = le_u64(data, 0); // location.objectid (start of the 17-byte key)
356    let type_byte = u8_at(data, dir_off::TYPE);
357    let name_len = le_u16(data, dir_off::NAME_LEN) as usize;
358
359    // The name occupies `name_len` bytes right after the fixed prefix (the
360    // optional xattr `data_len` bytes follow it). Clamp the name to the bytes
361    // actually present so a hostile `name_len` truncates rather than over-reads.
362    let name_start = dir_off::NAME;
363    let avail = data.len().saturating_sub(name_start);
364    let take = name_len.min(avail);
365    let name_bytes = data.get(name_start..name_start + take).unwrap_or(&[]);
366
367    Some(DirEntry {
368        name: String::from_utf8_lossy(name_bytes).into_owned(),
369        child,
370        item_type: DirItemType::from_byte(type_byte),
371    })
372}
373
374/// Resolve a slash-separated `path` to `(objectid, Inode)` within an FS_TREE
375/// `leaf`, starting from the FS_TREE root directory ([`FS_TREE_ROOT_DIR_OBJECTID`]).
376///
377/// A leading slash is optional and redundant / trailing separators are ignored.
378/// The empty path (`""` or `"/"`) resolves to the root directory itself.
379/// Returns `None` if any component is missing or descends through a non-directory.
380#[must_use]
381pub fn read_by_path(leaf: &Node, path: &str) -> Option<(u64, Inode)> {
382    let mut current = FS_TREE_ROOT_DIR_OBJECTID;
383    let mut inode = read_inode(leaf, current)?;
384
385    for component in path.split('/').filter(|c| !c.is_empty()) {
386        // To descend, the current inode must be a directory.
387        if !inode.is_dir() {
388            return None;
389        }
390        let entry = list_dir(leaf, current)
391            .into_iter()
392            .find(|e| e.name == component)?;
393        current = entry.child;
394        inode = read_inode(leaf, current)?;
395    }
396
397    Some((current, inode))
398}