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}