Skip to main content

apfs_core/
btree.rs

1//! Generic B-tree node walker (`btree_node_phys_t`, types `OBJECT_TYPE_BTREE
2//! 0x2` / `OBJECT_TYPE_BTREE_NODE 0x3`).
3//!
4//! Every APFS index (omap, fs-tree, snapshot-metadata tree, extent-reference
5//! tree, fext-tree) is a B-tree built from `btree_node_phys_t` blocks. The node
6//! header (Apple *APFS Reference*, after the 32-byte `obj_phys_t`):
7//!
8//! | off | size | field            |
9//! |-----|------|------------------|
10//! | 32  | 2    | `btn_flags`      |
11//! | 34  | 2    | `btn_level` (0 = leaf) |
12//! | 36  | 4    | `btn_nkeys`      |
13//! | 40  | 4    | `btn_table_space`  (`nloc_t`: u16 off, u16 len) |
14//! | 44  | 4    | `btn_free_space`   (`nloc_t`) |
15//! | 48  | 4    | `btn_key_free_list`(`nloc_t`) |
16//! | 52  | 4    | `btn_val_free_list`(`nloc_t`) |
17//! | 56  | …    | `btn_data[]`     |
18//!
19//! `btn_table_space.off` is relative to the start of `btn_data` (offset 56) and
20//! locates the table-of-contents (TOC). For **fixed-size** nodes
21//! (`BTNODE_FIXED_KV_SIZE` in `btn_flags`) a TOC entry is 4 bytes
22//! (`key_offs u16`, `value_offs u16`); for **variable-size** nodes it is 8 bytes
23//! (`key_offs`, `key_len`, `value_offs`, `value_len`, all u16).
24//!
25//! In a TOC entry, `key_offs` is relative to the **end of the TOC** (start of the
26//! key area) and `value_offs` is a **reversed** offset relative to the start of
27//! the B-tree footer (`btree_info_t`, 40 bytes) on a root node, or the end of the
28//! node on a non-root node. Offsets are verified verbatim against the Apple
29//! reference and the libfsapfs format spec.
30//!
31//! Walks are cycle-guarded and allocation-capped (`btn_nkeys` and every TOC
32//! offset are range-checked against the node before use — a hostile image's TOC
33//! is a classic out-of-bounds vector).
34
35/// `btn_flags` bit: node uses fixed-size key/value entries (`BTNODE_FIXED_KV_SIZE`).
36const BTNODE_FIXED_KV_SIZE: u16 = 0x4;
37/// `btn_flags` bit: node is the B-tree root (`BTNODE_ROOT`).
38const BTNODE_ROOT: u16 = 0x1;
39/// `btn_flags` bit: node is a leaf (`BTNODE_LEAF`).
40const BTNODE_LEAF: u16 = 0x2;
41
42// Node-header field offsets after the 32-byte `obj_phys_t`.
43const OFF_BTN_FLAGS: usize = 32;
44const OFF_BTN_LEVEL: usize = 34;
45const OFF_BTN_NKEYS: usize = 36;
46const OFF_BTN_TABLE_SPACE: usize = 40;
47/// `btn_data[]` begins immediately after the four `nloc_t` regions.
48const BTN_DATA_OFF: usize = 56;
49/// Minimum readable node-header length.
50const BTREE_NODE_MIN_LEN: usize = BTN_DATA_OFF;
51
52/// `btree_info_t` footer size (root node only).
53const BTREE_INFO_LEN: usize = 40;
54
55/// Fixed-size TOC entry length (`kvoff_t`): `key_offs`, `value_offs` (u16 each).
56const TOC_FIXED_LEN: usize = 4;
57/// Variable-size TOC entry length (`kvloc_t`): adds `key_len`, `value_len`.
58const TOC_VAR_LEN: usize = 8;
59
60/// Hard cap on `btn_nkeys` — a 4 `KiB` node cannot hold more than ~1000 entries;
61/// cap well above any legal node to reject an allocation-bomb `btn_nkeys`
62/// without rejecting a legal node.
63const MAX_BTN_NKEYS: u32 = 1 << 20;
64
65/// The B-tree subtype, which fixes the key/value sizes of a **fixed-size** node.
66///
67/// APFS B-trees are typed by `o_subtype`. For fixed-KV trees the per-entry sizes
68/// are not stored in the TOC, so the subtype supplies them. (Variable-KV trees
69/// carry the lengths in the TOC and ignore these.)
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71#[non_exhaustive]
72pub enum BTreeSubtype {
73    /// Object map (`OBJECT_TYPE_OMAP`): `omap_key_t` (16) → `omap_val_t` (16) at
74    /// a leaf; a branch value is an 8-byte child block number. A **fixed-KV**
75    /// tree, so the per-entry sizes below are consulted.
76    Omap,
77    /// File-system tree (`FSTREE`): `j_key`-keyed records of varying length. A
78    /// **variable-KV** tree — the TOC carries the per-entry key/value sizes, so
79    /// the fixed sizes below are *not* consulted for leaves; for an index node a
80    /// branch value is an 8-byte child object id (virtual, resolved through the
81    /// volume omap by the caller).
82    FsTree,
83}
84
85impl BTreeSubtype {
86    /// Fixed key length for a **leaf** entry of this subtype (fixed-KV trees only;
87    /// variable-KV trees read the length from the TOC and ignore this).
88    pub(crate) const fn fixed_key_len(self) -> usize {
89        match self {
90            BTreeSubtype::Omap => 16,  // omap_key_t { ok_oid u64, ok_xid u64 }
91            BTreeSubtype::FsTree => 0, // variable-KV: TOC carries the length
92        }
93    }
94
95    /// Fixed value length for a **leaf** entry of this subtype (fixed-KV only).
96    pub(crate) const fn fixed_leaf_val_len(self) -> usize {
97        match self {
98            BTreeSubtype::Omap => 16,  // omap_val_t { ov_flags, ov_size, ov_paddr }
99            BTreeSubtype::FsTree => 0, // variable-KV: TOC carries the length
100        }
101    }
102
103    /// Fixed value length for a **branch** (index) entry: an 8-byte child oid
104    /// (block number for a physical tree, virtual oid for the fs-tree).
105    pub(crate) const fn fixed_branch_val_len(self) -> usize {
106        match self {
107            BTreeSubtype::Omap | BTreeSubtype::FsTree => 8,
108        }
109    }
110}
111
112/// A parsed B-tree node header.
113#[derive(Debug, Clone, Copy)]
114#[non_exhaustive]
115pub struct BTreeNodeHeader {
116    /// `btn_flags`.
117    pub flags: u16,
118    /// `btn_level` (0 = leaf).
119    pub level: u16,
120    /// `btn_nkeys`.
121    pub nkeys: u32,
122}
123
124impl BTreeNodeHeader {
125    /// Whether this node is a leaf (`BTNODE_LEAF` set, or `btn_level == 0`).
126    #[must_use]
127    pub fn is_leaf(&self) -> bool {
128        self.flags & BTNODE_LEAF != 0 || self.level == 0
129    }
130
131    /// Whether this node is the B-tree root (`BTNODE_ROOT`).
132    #[must_use]
133    pub fn is_root(&self) -> bool {
134        self.flags & BTNODE_ROOT != 0
135    }
136
137    /// Whether entries are fixed-size (`BTNODE_FIXED_KV_SIZE`).
138    #[must_use]
139    pub fn is_fixed_kv(&self) -> bool {
140        self.flags & BTNODE_FIXED_KV_SIZE != 0
141    }
142}
143
144/// A key/value pair yielded by a node walk (borrowed from the node buffer).
145pub struct Entry<'a> {
146    /// The raw key bytes.
147    pub key: &'a [u8],
148    /// The raw value bytes.
149    pub value: &'a [u8],
150}
151
152/// Parse a node header (bounds-checked). `None` if the block is too short to hold
153/// the header (never panics).
154#[must_use]
155pub fn parse_node_header(block: &[u8]) -> Option<BTreeNodeHeader> {
156    if block.len() < BTREE_NODE_MIN_LEN {
157        return None;
158    }
159    Some(BTreeNodeHeader {
160        flags: crate::bytes::le_u16(block, OFF_BTN_FLAGS),
161        level: crate::bytes::le_u16(block, OFF_BTN_LEVEL),
162        nkeys: crate::bytes::le_u32(block, OFF_BTN_NKEYS),
163    })
164}
165
166/// Iterate the (key, value) entries of a single node, handling both fixed and
167/// variable KV layouts. Returns an empty vector on a malformed node (a short
168/// block, an out-of-bounds TOC, or an entry whose key/value slice would run past
169/// the node) — never panics, never reads out of bounds.
170///
171/// `subtype` supplies the fixed key/value sizes for a fixed-KV node; variable-KV
172/// nodes read the sizes from the TOC and ignore it.
173#[must_use]
174pub fn node_entries(block: &[u8], subtype: BTreeSubtype) -> Vec<Entry<'_>> {
175    let Some(hdr) = parse_node_header(block) else {
176        return Vec::new(); // cov:unreachable: callers pass full-size node blocks
177    };
178    let nkeys = hdr.nkeys.min(MAX_BTN_NKEYS) as usize;
179    if nkeys == 0 {
180        return Vec::new();
181    }
182
183    // The TOC begins at btn_data + btn_table_space.off; the key area begins right
184    // after the TOC (btn_table_space.len bytes long).
185    let toc_off = crate::bytes::le_u16(block, OFF_BTN_TABLE_SPACE) as usize;
186    let toc_len = crate::bytes::le_u16(block, OFF_BTN_TABLE_SPACE + 2) as usize;
187    let toc_start = BTN_DATA_OFF + toc_off; // u16 off + 56 cannot overflow usize
188    let key_area = toc_start + toc_len; // u16 sums cannot overflow usize
189
190    // Value offsets are reversed from the start of the footer on a root node, or
191    // the end of the node otherwise.
192    let val_base = if hdr.is_root() {
193        block.len().saturating_sub(BTREE_INFO_LEN)
194    } else {
195        block.len()
196    };
197
198    let fixed = hdr.is_fixed_kv();
199    let entry_len = if fixed { TOC_FIXED_LEN } else { TOC_VAR_LEN };
200    let key_len = subtype.fixed_key_len();
201    let val_len = if hdr.is_leaf() {
202        subtype.fixed_leaf_val_len()
203    } else {
204        subtype.fixed_branch_val_len()
205    };
206
207    let mut out = Vec::with_capacity(nkeys);
208    for i in 0..nkeys {
209        let e = toc_start + i * entry_len; // bounded by MAX_BTN_NKEYS * 8
210                                           // A TOC entry must lie within the key area (before the keys begin) and
211                                           // within the node.
212        if e + entry_len > key_area || e + entry_len > block.len() {
213            break;
214        }
215        // TOC layout (verified vs Apple reference + libfsapfs):
216        //   fixed (4 B):    key_offs u16 @0, value_offs u16 @2
217        //   variable (8 B): key_offs u16 @0, key_len u16 @2,
218        //                   value_offs u16 @4, value_len u16 @6
219        let koff = crate::bytes::le_u16(block, e) as usize;
220        let (voff, this_key_len, this_val_len) = if fixed {
221            (
222                crate::bytes::le_u16(block, e + 2) as usize,
223                key_len,
224                val_len,
225            )
226        } else {
227            (
228                crate::bytes::le_u16(block, e + 4) as usize,
229                crate::bytes::le_u16(block, e + 2) as usize,
230                crate::bytes::le_u16(block, e + 6) as usize,
231            )
232        };
233
234        // Key slice: [key_area + koff, +len); value slice: val_base - voff
235        // growing backward by this_val_len. Every bound is checked against the
236        // node; a hostile offset yields a skipped entry, never an OOB read.
237        let kstart = key_area + koff; // u16 sums cannot overflow usize
238        let kend = kstart + this_key_len;
239        let Some(vstart) = val_base.checked_sub(voff) else {
240            continue;
241        };
242        let vend = vstart + this_val_len;
243
244        let (Some(key), Some(value)) = (block.get(kstart..kend), block.get(vstart..vend)) else {
245            continue;
246        };
247        out.push(Entry { key, value });
248    }
249    out
250}
251
252/// Depth cap on a root→leaf descent (cyclic-oid / malformed-tree guard).
253const MAX_BTREE_DEPTH: usize = 64;
254
255/// Walk a physically-stored B-tree from its root block, invoking `visit` for
256/// every **leaf** (key, value) entry. Index nodes are descended by their child
257/// block numbers; each node's Fletcher-64 checksum is verified before its TOC is
258/// trusted, the descent depth is capped (`MAX_BTREE_DEPTH`), and a visited-set
259/// guards against revisiting a block (cyclic-oid defense). The container omap
260/// tree is stored physically, so `root_paddr` and every child block number are
261/// direct block addresses.
262///
263/// # Errors
264/// [`crate::ApfsError::ChecksumMismatch`] for a node whose checksum fails;
265/// [`crate::ApfsError::CycleGuard`] on a cycle or an over-deep tree;
266/// [`crate::ApfsError::Io`] on a read/seek failure.
267pub fn for_each_leaf_entry<R, F>(
268    reader: &mut R,
269    root_paddr: u64,
270    block_size: usize,
271    subtype: BTreeSubtype,
272    visit: &mut F,
273) -> crate::Result<()>
274where
275    R: std::io::Read + std::io::Seek,
276    F: FnMut(&[u8], &[u8]),
277{
278    let mut visited = std::collections::HashSet::new();
279    descend(
280        reader,
281        root_paddr,
282        block_size,
283        subtype,
284        0,
285        &mut visited,
286        visit,
287    )
288}
289
290/// Read a block and verify its Fletcher-64 checksum before returning it.
291/// `Ok(None)` only for the (unreachable) `paddr * block_size` overflow — the
292/// caller treats it as a clean skip, mirroring the full-walk descent.
293fn read_verified_node<R: std::io::Read + std::io::Seek>(
294    reader: &mut R,
295    paddr: u64,
296    block_size: usize,
297) -> crate::Result<Option<Vec<u8>>> {
298    let mut buf = vec![0u8; block_size];
299    let Some(offset) = paddr.checked_mul(block_size as u64) else {
300        return Ok(None); // cov:unreachable: in-image paddr*bs cannot overflow u64
301    };
302    reader.seek(std::io::SeekFrom::Start(offset))?;
303    reader.read_exact(&mut buf)?;
304
305    // Checksum-before-trust: never read a node's TOC until its cksum validates.
306    let stored = crate::object::fletcher64_stored(&buf);
307    let computed = crate::object::fletcher64_checksum(&buf);
308    if stored != computed {
309        let block = crate::object::ObjPhys::parse(&buf).map_or(paddr, |h| h.oid);
310        return Err(crate::ApfsError::ChecksumMismatch {
311            block,
312            stored,
313            computed,
314        });
315    }
316    Ok(Some(buf))
317}
318
319/// Descend a physically-stored B-tree to the single leaf whose key range covers
320/// the search target, invoking `visit` on that leaf's entries. `cmp(entry_key)`
321/// returns how an entry's key orders against the target. At each index node the
322/// descent follows the last child whose separator key is ≤ the target (standard
323/// B-tree point lookup); the caller's `visit` filters the landing leaf. Same
324/// checksum / depth-cap / visited-set guards as [`for_each_leaf_entry`].
325///
326/// This is the keyed counterpart to [`for_each_leaf_entry`] — it reads one
327/// root→leaf path instead of every node, for hot point lookups like
328/// [`crate::omap::ObjectMap::resolve`]. The B-tree must be physically stored
329/// (children are direct block numbers), like the omap tree.
330///
331/// # Errors
332/// [`crate::ApfsError::ChecksumMismatch`] for a node whose checksum fails;
333/// [`crate::ApfsError::CycleGuard`] on a cycle or an over-deep tree;
334/// [`crate::ApfsError::Io`] on a read/seek failure.
335pub fn find_leaf<R, C, F>(
336    reader: &mut R,
337    root_paddr: u64,
338    block_size: usize,
339    subtype: BTreeSubtype,
340    cmp: C,
341    visit: &mut F,
342) -> crate::Result<()>
343where
344    R: std::io::Read + std::io::Seek,
345    C: Fn(&[u8]) -> std::cmp::Ordering,
346    F: FnMut(&[u8], &[u8]),
347{
348    let mut visited = std::collections::HashSet::new();
349    let mut paddr = root_paddr;
350    for _ in 0..MAX_BTREE_DEPTH {
351        // A block visited twice is a cycle — reject rather than loop forever.
352        if !visited.insert(paddr) {
353            return Err(crate::ApfsError::CycleGuard {
354                cap: MAX_BTREE_DEPTH,
355            });
356        }
357        let Some(buf) = read_verified_node(reader, paddr, block_size)? else {
358            return Ok(()); // cov:unreachable: in-image paddr*bs cannot overflow u64
359        };
360        let Some(hdr) = parse_node_header(&buf) else {
361            return Ok(()); // cov:unreachable: buf is block_size >= node header length
362        };
363
364        if hdr.is_leaf() {
365            for e in node_entries(&buf, subtype) {
366                visit(e.key, e.value);
367            }
368            return Ok(());
369        }
370
371        // Index node: separator key i is the smallest key of child i's subtree, so
372        // the child covering the target is the last one whose key is ≤ the target
373        // (or the first child if the target precedes every separator).
374        let entries = node_entries(&buf, subtype);
375        let Some(first) = entries.first() else {
376            return Ok(()); // cov:unreachable: a non-leaf node has ≥1 child
377        };
378        let mut child = crate::bytes::le_u64(first.value, 0);
379        for e in &entries {
380            if cmp(e.key) == std::cmp::Ordering::Greater {
381                break;
382            }
383            child = crate::bytes::le_u64(e.value, 0);
384        }
385        paddr = child;
386    }
387    Err(crate::ApfsError::CycleGuard {
388        cap: MAX_BTREE_DEPTH,
389    })
390}
391
392fn descend<R, F>(
393    reader: &mut R,
394    paddr: u64,
395    block_size: usize,
396    subtype: BTreeSubtype,
397    depth: usize,
398    visited: &mut std::collections::HashSet<u64>,
399    visit: &mut F,
400) -> crate::Result<()>
401where
402    R: std::io::Read + std::io::Seek,
403    F: FnMut(&[u8], &[u8]),
404{
405    if depth >= MAX_BTREE_DEPTH {
406        return Err(crate::ApfsError::CycleGuard {
407            cap: MAX_BTREE_DEPTH,
408        });
409    }
410    // A block visited twice is a cycle — reject rather than loop forever.
411    if !visited.insert(paddr) {
412        return Err(crate::ApfsError::CycleGuard {
413            cap: MAX_BTREE_DEPTH,
414        });
415    }
416
417    let Some(buf) = read_verified_node(reader, paddr, block_size)? else {
418        return Ok(()); // cov:unreachable: in-image paddr*bs cannot overflow u64
419    };
420
421    let Some(hdr) = parse_node_header(&buf) else {
422        return Ok(()); // cov:unreachable: buf is block_size >= node header length
423    };
424
425    if hdr.is_leaf() {
426        for e in node_entries(&buf, subtype) {
427            visit(e.key, e.value);
428        }
429        return Ok(());
430    }
431
432    // Index node: each value is an 8-byte child block number; descend each.
433    let children: Vec<u64> = node_entries(&buf, subtype)
434        .iter()
435        .map(|e| crate::bytes::le_u64(e.value, 0))
436        .collect();
437    for child in children {
438        descend(
439            reader,
440            child,
441            block_size,
442            subtype,
443            depth + 1,
444            visited,
445            visit,
446        )?;
447    }
448    Ok(())
449}