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}