Skip to main content

btrfs_core/
node.rs

1//! btrfs B-tree node/leaf parsing + the chunk-tree logical→physical `ChunkMap`.
2//!
3//! Every btrfs metadata block (a `nodesize`-byte node, default 16384) begins
4//! with a `btrfs_header`, then — by `level` — either leaf items (`level == 0`)
5//! or interior key-pointers (`level > 0`). All fields are little-endian.
6//!
7//! Layouts were transcribed from the on-disk `btrfs_header` / `btrfs_item` /
8//! `btrfs_key_ptr` / `btrfs_chunk` / `btrfs_stripe` and **verified byte-for-byte
9//! against `btrfs inspect-internal dump-tree`** on the minted oracle image (see
10//! `tests/data/README.md`, "P1 ground truth"). The verified header size is
11//! [`BTRFS_HEADER_SIZE`] = 101 bytes.
12//!
13//! # Safety
14//!
15//! This parses untrusted, attacker-controllable images. Every read is
16//! bounds-checked (`crate::bytes` helpers yield 0 out of range), `nritems` is
17//! capped by how many items/pointers the block can physically hold, and a
18//! lying `data_offset`/`data_size` yields an empty slice rather than an
19//! over-read. Node parsing is **non-fatal on a bad checksum**: a tampered block
20//! still parses and surfaces `crc_valid == Some(false)` so the `-forensic` layer
21//! can turn it into a Finding.
22
23use crate::bytes::{le_u16, le_u32, le_u64, u8_at};
24use crate::chunk::{Stripe, DISK_KEY_SIZE, STRIPE_SIZE};
25use crate::crc::{superblock_crc_status, CsumType};
26use crate::error::BtrfsError;
27use crate::superblock::Superblock;
28use crate::DiskKey;
29
30/// `sizeof(btrfs_header)` on disk: `csum[32] + fsid[16] + bytenr(u64) +
31/// flags(u64) + chunk_tree_uuid[16] + generation(u64) + owner(u64) +
32/// nritems(u32) + level(u8)` = 101 bytes.
33pub const BTRFS_HEADER_SIZE: usize = 101;
34
35/// `sizeof(btrfs_item)`: `disk_key[17] + offset(u32) + size(u32)` = 25 bytes.
36/// `offset` is the item's data start **relative to the end of the header**;
37/// `size` is the item data length.
38pub const BTRFS_ITEM_SIZE: usize = DISK_KEY_SIZE + 4 + 4;
39
40/// `sizeof(btrfs_key_ptr)`: `disk_key[17] + blockptr(u64) + generation(u64)` =
41/// 33 bytes.
42pub const BTRFS_KEY_PTR_SIZE: usize = DISK_KEY_SIZE + 8 + 8;
43
44/// The `btrfs_chunk` fixed header (up to the variable stripe array):
45/// `length,owner,stripe_len,type` (4×u64) + `io_align,io_width,sector_size`
46/// (3×u32) + `num_stripes,sub_stripes` (2×u16) = 48 bytes.
47pub const CHUNK_HEADER_SIZE: usize = 48;
48
49/// `BTRFS_CHUNK_TREE_OBJECTID` — the owner tree id of a chunk-tree node (3).
50pub const CHUNK_TREE_OBJECTID: u64 = 3;
51
52/// The offset of each `btrfs_header` scalar (little-endian on disk).
53mod hdr {
54    pub const BYTENR: usize = 0x30;
55    pub const FLAGS: usize = 0x38;
56    pub const CHUNK_TREE_UUID: usize = 0x40;
57    pub const GENERATION: usize = 0x50;
58    pub const OWNER: usize = 0x58;
59    pub const NRITEMS: usize = 0x60;
60    pub const LEVEL: usize = 0x64;
61    pub const FSID: usize = 0x20;
62}
63
64/// A parsed `btrfs_header` — the fixed 101-byte prefix of every node/leaf.
65#[derive(Debug, Clone, PartialEq, Eq)]
66#[non_exhaustive]
67pub struct Header {
68    /// `csum` — the first 4 bytes of the 32-byte checksum field (the crc32c
69    /// digest for a crc32c filesystem), verbatim.
70    pub csum: [u8; 4],
71    /// `fsid` (offset 0x20) — the filesystem UUID.
72    pub fsid: [u8; 16],
73    /// `bytenr` (offset 0x30) — **this node's own logical address**.
74    pub bytenr: u64,
75    /// `flags` (offset 0x38).
76    pub flags: u64,
77    /// `chunk_tree_uuid` (offset 0x40).
78    pub chunk_tree_uuid: [u8; 16],
79    /// `generation` (offset 0x50) — the transaction id that wrote this block
80    /// (the CoW/staleness lever: an older generation marks a stale block).
81    pub generation: u64,
82    /// `owner` (offset 0x58) — the objectid of the tree this block belongs to
83    /// (1 = ROOT_TREE, 3 = CHUNK_TREE, …).
84    pub owner: u64,
85    /// `nritems` (offset 0x60) — the number of items (leaf) or key-pointers
86    /// (interior) that follow the header.
87    pub nritems: u32,
88    /// `level` (offset 0x64) — 0 = leaf, > 0 = interior node.
89    pub level: u8,
90}
91
92impl Header {
93    /// Decode the 101-byte header at the start of `block`, or `None` if `block`
94    /// is shorter than [`BTRFS_HEADER_SIZE`].
95    #[must_use]
96    fn parse(block: &[u8]) -> Option<Self> {
97        if block.len() < BTRFS_HEADER_SIZE {
98            return None;
99        }
100        let mut csum = [0u8; 4];
101        for (i, b) in csum.iter_mut().enumerate() {
102            *b = u8_at(block, i);
103        }
104        let mut fsid = [0u8; 16];
105        for (i, b) in fsid.iter_mut().enumerate() {
106            *b = u8_at(block, hdr::FSID + i);
107        }
108        let mut chunk_tree_uuid = [0u8; 16];
109        for (i, b) in chunk_tree_uuid.iter_mut().enumerate() {
110            *b = u8_at(block, hdr::CHUNK_TREE_UUID + i);
111        }
112        Some(Header {
113            csum,
114            fsid,
115            bytenr: le_u64(block, hdr::BYTENR),
116            flags: le_u64(block, hdr::FLAGS),
117            chunk_tree_uuid,
118            generation: le_u64(block, hdr::GENERATION),
119            owner: le_u64(block, hdr::OWNER),
120            nritems: le_u32(block, hdr::NRITEMS),
121            level: u8_at(block, hdr::LEVEL),
122        })
123    }
124}
125
126/// A `btrfs_key_ptr` from an interior node: the child's smallest key, its
127/// logical block address, and the generation it was written in.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct KeyPtr {
130    /// The child subtree's smallest key.
131    pub key: DiskKey,
132    /// `blockptr` — the child node's **logical** address.
133    pub blockptr: u64,
134    /// `generation` — the transaction id the child was written in.
135    pub generation: u64,
136}
137
138/// A parsed `btrfs_chunk` item: chunk geometry + its physical stripes.
139///
140/// The chunk's logical start is its item key's `offset` (carried alongside in
141/// [`Node::chunk_items`]), not stored inside the chunk struct.
142#[derive(Debug, Clone, PartialEq, Eq)]
143#[non_exhaustive]
144pub struct Chunk {
145    /// `length` — the chunk's logical byte span.
146    pub length: u64,
147    /// `owner` — the objectid that owns the chunk (2 = the extent tree).
148    pub owner: u64,
149    /// `stripe_len` — the stripe unit size.
150    pub stripe_len: u64,
151    /// `type` — the `BTRFS_BLOCK_GROUP_*` flag bits (DATA/SYSTEM/METADATA | DUP…).
152    pub chunk_type: u64,
153    /// `num_stripes` — the number of physical stripes that follow.
154    pub num_stripes: u16,
155    /// `sub_stripes` — RAID10 sub-stripe count (1 for single/DUP).
156    pub sub_stripes: u16,
157    /// The decoded stripes (physical placements).
158    pub stripes: Vec<Stripe>,
159}
160
161impl Chunk {
162    /// Decode a `btrfs_chunk` from `data` (a CHUNK_ITEM's item data). Bounds- and
163    /// count-checked: an absurd `num_stripes` is capped by the bytes available,
164    /// so a malformed item truncates rather than over-reads.
165    #[must_use]
166    fn parse(data: &[u8]) -> Self {
167        let num_stripes = le_u16(data, 44);
168        // Cap the declared stripe count by how many actually fit in `data`.
169        let avail = data.len().saturating_sub(CHUNK_HEADER_SIZE);
170        let max_fit = avail / STRIPE_SIZE;
171        let take = usize::from(num_stripes).min(max_fit);
172
173        let mut stripes = Vec::with_capacity(take);
174        for i in 0..take {
175            let so = CHUNK_HEADER_SIZE + i * STRIPE_SIZE;
176            let mut dev_uuid = [0u8; 16];
177            for (j, b) in dev_uuid.iter_mut().enumerate() {
178                *b = u8_at(data, so + 16 + j);
179            }
180            stripes.push(Stripe {
181                devid: le_u64(data, so),
182                offset: le_u64(data, so + 8),
183                dev_uuid,
184            });
185        }
186        Chunk {
187            length: le_u64(data, 0),
188            owner: le_u64(data, 8),
189            stripe_len: le_u64(data, 16),
190            chunk_type: le_u64(data, 24),
191            num_stripes,
192            sub_stripes: le_u16(data, 46),
193            stripes,
194        }
195    }
196}
197
198/// The decoded body of a node: leaf items or interior key-pointers.
199#[derive(Debug, Clone, PartialEq, Eq)]
200enum Body {
201    /// A leaf (`level == 0`): the item headers (each carries its data offset +
202    /// size relative to the end of the node header).
203    Leaf(Vec<ItemHeader>),
204    /// An interior node (`level > 0`): the child key-pointers.
205    Interior(Vec<KeyPtr>),
206}
207
208/// A decoded `btrfs_item` header (the key + where its data lives in the block).
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210struct ItemHeader {
211    key: DiskKey,
212    /// Data start **relative to the end of the header** (`itemoff` in dump-tree).
213    data_offset: u32,
214    /// Data length.
215    data_size: u32,
216}
217
218/// A parsed btrfs B-tree node (leaf or interior), plus its non-fatal checksum
219/// status. The raw block bytes are retained so leaf item data can be sliced
220/// on demand (bounds-checked).
221#[derive(Debug, Clone, PartialEq, Eq)]
222#[non_exhaustive]
223pub struct Node {
224    /// The decoded 101-byte header.
225    pub header: Header,
226    /// The crc32c status over `[0x20 .. block.len()]`: `Some(true)` if the
227    /// stored digest verifies, `Some(false)` if it does not (corrupt/tampered),
228    /// or `None` for a non-crc32c checksum whose verifier is deferred.
229    pub crc_valid: Option<bool>,
230    body: Body,
231    block: Vec<u8>,
232}
233
234impl Node {
235    /// Parse a `nodesize`-byte btrfs node from the start of `block`.
236    ///
237    /// The block should be exactly one node (`nodesize` bytes); the checksum is
238    /// computed over `[0x20 .. block.len()]`, so pass the whole node block. A
239    /// bad checksum does **not** fail the parse — it is surfaced in
240    /// [`Node::crc_valid`].
241    ///
242    /// # Errors
243    ///
244    /// - [`BtrfsError::Truncated`] if `block` is shorter than the 101-byte
245    ///   header.
246    pub fn parse(block: &[u8]) -> Result<Self, BtrfsError> {
247        let Some(header) = Header::parse(block) else {
248            return Err(BtrfsError::Truncated {
249                structure: "btrfs_header",
250                need: BTRFS_HEADER_SIZE,
251                have: block.len(),
252            });
253        };
254
255        // The node checksum covers the block after the 32-byte csum field to the
256        // end of the block (btrfs default crc32c). csum_type is a filesystem
257        // property; a standalone node cannot carry it, so P1 assumes the mkfs
258        // default (crc32c). Non-crc32c filesystems arrive with later phases.
259        let crc_valid = superblock_crc_status(CsumType::Crc32c, block, block.len());
260
261        let nritems = header.nritems as usize;
262        let body = if header.level == 0 {
263            Body::Leaf(parse_items(block, nritems))
264        } else {
265            Body::Interior(parse_key_ptrs(block, nritems))
266        };
267
268        Ok(Node {
269            header,
270            crc_valid,
271            body,
272            block: block.to_vec(),
273        })
274    }
275
276    /// `true` if this is a leaf node (`level == 0`).
277    #[must_use]
278    pub fn is_leaf(&self) -> bool {
279        matches!(self.body, Body::Leaf(_))
280    }
281
282    /// Iterate `(key, data)` over a leaf's items. Each data slice is
283    /// bounds-checked against the block: a lying `data_offset`/`data_size`
284    /// yields an empty slice, never an over-read. An interior node yields
285    /// nothing.
286    pub fn leaf_items(&self) -> impl Iterator<Item = (DiskKey, &[u8])> {
287        let items: &[ItemHeader] = match &self.body {
288            Body::Leaf(items) => items,
289            Body::Interior(_) => &[],
290        };
291        items.iter().map(move |it| (it.key, self.item_data(it)))
292    }
293
294    /// The bounds-checked data slice for one leaf item. Data lives at
295    /// `BTRFS_HEADER_SIZE + data_offset` for `data_size` bytes; an out-of-range
296    /// range yields an empty slice.
297    fn item_data(&self, it: &ItemHeader) -> &[u8] {
298        let start = BTRFS_HEADER_SIZE.saturating_add(it.data_offset as usize);
299        let end = start.saturating_add(it.data_size as usize);
300        self.block.get(start..end).unwrap_or(&[])
301    }
302
303    /// The interior node's child key-pointers. A leaf yields an empty slice.
304    #[must_use]
305    pub fn key_ptrs(&self) -> &[KeyPtr] {
306        match &self.body {
307            Body::Interior(ptrs) => ptrs,
308            Body::Leaf(_) => &[],
309        }
310    }
311
312    /// Decode every CHUNK_ITEM in a leaf into `(logical_start, Chunk)` pairs
313    /// (the logical start is the item key's `offset`). Non-CHUNK items (e.g.
314    /// DEV_ITEM) are skipped. An interior node yields nothing.
315    #[must_use]
316    pub fn chunk_items(&self) -> Vec<(u64, Chunk)> {
317        self.leaf_items()
318            .filter(|(key, _)| key.key_type == crate::CHUNK_ITEM_KEY)
319            .map(|(key, data)| (key.offset, Chunk::parse(data)))
320            .collect()
321    }
322}
323
324/// Parse up to `nritems` leaf item headers, stopping at the block bound so a
325/// lying `nritems` never over-reads (the item array grows forward from the
326/// header end; only the count is capped here, individual data is sliced later).
327fn parse_items(block: &[u8], nritems: usize) -> Vec<ItemHeader> {
328    // The item-header array cannot hold more than fits between the header end
329    // and the block end.
330    let avail = block.len().saturating_sub(BTRFS_HEADER_SIZE);
331    let max_fit = avail / BTRFS_ITEM_SIZE;
332    let take = nritems.min(max_fit);
333    let mut out = Vec::with_capacity(take);
334    for i in 0..take {
335        let io = BTRFS_HEADER_SIZE + i * BTRFS_ITEM_SIZE;
336        out.push(ItemHeader {
337            key: DiskKey::parse(block, io),
338            data_offset: le_u32(block, io + DISK_KEY_SIZE),
339            data_size: le_u32(block, io + DISK_KEY_SIZE + 4),
340        });
341    }
342    out
343}
344
345/// Parse up to `nritems` interior key-pointers, capped by the block bound.
346fn parse_key_ptrs(block: &[u8], nritems: usize) -> Vec<KeyPtr> {
347    let avail = block.len().saturating_sub(BTRFS_HEADER_SIZE);
348    let max_fit = avail / BTRFS_KEY_PTR_SIZE;
349    let take = nritems.min(max_fit);
350    let mut out = Vec::with_capacity(take);
351    for i in 0..take {
352        let po = BTRFS_HEADER_SIZE + i * BTRFS_KEY_PTR_SIZE;
353        out.push(KeyPtr {
354            key: DiskKey::parse(block, po),
355            blockptr: le_u64(block, po + DISK_KEY_SIZE),
356            generation: le_u64(block, po + DISK_KEY_SIZE + 8),
357        });
358    }
359    out
360}
361
362/// One logical→physical mapping entry: a chunk's logical span and its stripes.
363#[derive(Debug, Clone, PartialEq, Eq)]
364struct MapEntry {
365    logical_start: u64,
366    length: u64,
367    /// `(devid, physical_offset)` of each stripe; single/DUP use the first.
368    stripes: Vec<(u64, u64)>,
369}
370
371/// The chunk-tree logical→physical map: the union of every CHUNK_ITEM's span
372/// and its physical placement. Covers single-device profiles (SINGLE/DUP);
373/// for DUP the first stripe (first mirror) is returned.
374#[derive(Debug, Clone, Default, PartialEq, Eq)]
375pub struct ChunkMap {
376    entries: Vec<MapEntry>,
377}
378
379impl ChunkMap {
380    /// An empty map.
381    #[must_use]
382    pub fn new() -> Self {
383        ChunkMap {
384            entries: Vec::new(),
385        }
386    }
387
388    /// Add every CHUNK_ITEM found in a chunk-tree leaf `node` to the map.
389    pub fn add_from_node(&mut self, node: &Node) {
390        for (logical, chunk) in node.chunk_items() {
391            self.entries.push(MapEntry {
392                logical_start: logical,
393                length: chunk.length,
394                stripes: chunk.stripes.iter().map(|s| (s.devid, s.offset)).collect(),
395            });
396        }
397    }
398
399    /// Translate a logical address to `(devid, physical_byte)` on the first
400    /// stripe's device, for single-device single/DUP chunks. Returns `None` if
401    /// no chunk covers `logical`, or the covering chunk has no stripe.
402    #[must_use]
403    pub fn logical_to_physical(&self, logical: u64) -> Option<(u64, u64)> {
404        for e in &self.entries {
405            let end = e.logical_start.checked_add(e.length)?;
406            if logical >= e.logical_start && logical < end {
407                let (devid, phys_start) = *e.stripes.first()?;
408                let delta = logical - e.logical_start;
409                let phys = phys_start.checked_add(delta)?;
410                return Some((devid, phys));
411            }
412        }
413        None
414    }
415
416    /// Build the full chunk map by bootstrapping from the superblock's
417    /// `sys_chunk_array` and walking the chunk tree from `sb.chunk_root`.
418    ///
419    /// The `sys_chunk_array` covers the chunk_root's own logical range (that is
420    /// its whole purpose), so `chunk_root` is reachable; each subsequently read
421    /// node's `blockptr`s are translated through the map built *so far* (the
422    /// chunk tree is self-describing). On the oracle the chunk tree is a single
423    /// leaf, but interior nodes are followed recursively (bounded).
424    ///
425    /// # Errors
426    ///
427    /// - [`BtrfsError::Truncated`] if `sb.chunk_root` cannot be translated to a
428    ///   readable node within `image` (a failed bootstrap is loud, never an
429    ///   empty map — see the Paranoid Gatekeeper standard).
430    pub fn walk(image: &[u8], sb: &Superblock) -> Result<Self, BtrfsError> {
431        // Bootstrap map from the sys_chunk_array (seeds the chunk_root's range).
432        let mut boot = ChunkMap::new();
433        for c in &sb.sys_chunks {
434            boot.entries.push(MapEntry {
435                logical_start: c.key.offset,
436                length: c.length,
437                stripes: c.stripes.iter().map(|s| (s.devid, s.offset)).collect(),
438            });
439        }
440
441        let nodesize = sb.nodesize as usize;
442        let mut map = ChunkMap::new();
443
444        // Depth/visit budget guards against a corrupt/cyclic chunk tree.
445        let mut budget: usize = 4096;
446        let mut stack = vec![sb.chunk_root];
447        let mut bootstrapped_root = false;
448
449        while let Some(logical) = stack.pop() {
450            if budget == 0 {
451                break; // cov:unreachable: the oracle chunk tree is a single leaf; only a >4096-node tree exhausts this, not craftable as a fixture
452            }
453            budget -= 1;
454
455            // Translate via the map built so far, falling back to the bootstrap
456            // map (needed for the chunk_root itself and its own subtree).
457            let phys = map
458                .logical_to_physical(logical)
459                .or_else(|| boot.logical_to_physical(logical));
460            let Some((_devid, phys)) = phys else {
461                if !bootstrapped_root {
462                    // The very first (chunk_root) translation failed: the
463                    // bootstrap did not cover it — a loud, non-empty failure.
464                    return Err(BtrfsError::Truncated {
465                        structure: "chunk_root logical (sys_chunk_array bootstrap)",
466                        need: logical as usize,
467                        have: 0,
468                    });
469                } // cov:unreachable: the if-body's only statement is `return Err`, so this closing brace's fall-through region is unreachable
470                continue; // cov:unreachable: an interior blockptr not covered by the map built so far implies a malformed chunk tree; the oracle's is single-leaf and self-consistent
471            };
472            bootstrapped_root = true;
473
474            let start = phys as usize;
475            let Some(block) = image.get(start..start.saturating_add(nodesize)) else {
476                if map.entries.is_empty() {
477                    return Err(BtrfsError::Truncated {
478                        structure: "chunk_root node (out of image)",
479                        need: start.saturating_add(nodesize),
480                        have: image.len(),
481                    });
482                } // cov:unreachable: the if-body's only statement is `return Err`, so this closing brace's fall-through region is unreachable
483                continue; // cov:unreachable: a self-consistent chunk tree points only inside the image; guarded for a corrupt blockptr
484            };
485
486            let Ok(node) = Node::parse(block) else {
487                continue; // cov:unreachable: block is nodesize (>=101) bytes, so Header::parse always succeeds
488            };
489
490            if node.is_leaf() {
491                map.add_from_node(&node);
492            } else {
493                for kp in node.key_ptrs() {
494                    stack.push(kp.blockptr);
495                }
496            }
497        }
498
499        Ok(map)
500    }
501}
502
503/// Read the btrfs node at logical address `logical`: translate it to a physical
504/// offset via `chunk_map` (falling back to the superblock `sys_chunk_array`
505/// bootstrap for addresses inside the chunk_root's own range), slice `nodesize`
506/// bytes, and parse the header + items/pointers.
507///
508/// # Errors
509///
510/// - [`BtrfsError::Truncated`] if `logical` cannot be translated to a physical
511///   offset, or the resulting `nodesize`-byte block lies outside `image`.
512pub fn read_node(
513    image: &[u8],
514    sb: &Superblock,
515    chunk_map: &ChunkMap,
516    logical: u64,
517) -> Result<Node, BtrfsError> {
518    // Prefer the full chunk map; fall back to the sys_chunk_array bootstrap so
519    // the chunk_root (and its own range) is reachable before the tree is walked.
520    let phys = chunk_map.logical_to_physical(logical).or_else(|| {
521        sb.sys_chunks
522            .iter()
523            .find_map(|c| c.logical_to_physical(logical).map(|p| (0u64, p)))
524    });
525    let Some((_devid, phys)) = phys else {
526        return Err(BtrfsError::Truncated {
527            structure: "logical address (no chunk mapping)",
528            need: logical as usize,
529            have: 0,
530        });
531    };
532
533    let nodesize = sb.nodesize as usize;
534    let start = phys as usize;
535    let Some(block) = image.get(start..start.saturating_add(nodesize)) else {
536        return Err(BtrfsError::Truncated {
537            structure: "node block (out of image)",
538            need: start.saturating_add(nodesize),
539            have: image.len(),
540        });
541    };
542    Node::parse(block)
543}