apfs_core/checkpoint.rs
1//! Checkpoint descriptor + data ring, and resolution of the **live** container
2//! superblock.
3//!
4//! APFS writes superblocks transactionally into a ring of checkpoint blocks. To
5//! find the current container state (Apple *APFS Reference*, "Mounting an APFS
6//! container"): scan the checkpoint **descriptor area** for the
7//! `nx_superblock_t` with the **highest `xid` that also has a valid Fletcher-64
8//! checksum**, then read the checkpoint **map** (`checkpoint_map_phys_t`, type
9//! `OBJECT_TYPE_CHECKPOINT_MAP 0xc`) to resolve the ephemeral oids it references
10//! (spaceman, reaper) to physical addresses.
11//!
12//! Non-latest checkpoints reference superseded objects — this is normal
13//! copy-on-write history, and is a **recovery opportunity** (the analyzer reads
14//! it as residue, never as an anomaly).
15
16use crate::container::NX_MAXIMUM_BLOCK_SIZE;
17use crate::object::{fletcher64_checksum, fletcher64_stored, ObjPhys};
18
19/// Object type code `OBJECT_TYPE_NX_SUPERBLOCK` (Apple): `o_type & 0xffff == 1`.
20const OBJECT_TYPE_NX_SUPERBLOCK: u16 = 0x1;
21/// Object type code `OBJECT_TYPE_CHECKPOINT_MAP` (Apple): `o_type & 0xffff == 0xc`.
22const OBJECT_TYPE_CHECKPOINT_MAP: u16 = 0xc;
23
24/// `checkpoint_map_phys_t` field offsets after the 32-byte `obj_phys_t cpm_o`.
25const CPM_COUNT_OFF: usize = 36; // cpm_count u32
26const CPM_MAP_OFF: usize = 40; // cpm_map[] start
27/// One `checkpoint_mapping_t` is 40 bytes: `cpm_type`/`cpm_subtype`/`cpm_size`/
28/// `cpm_pad` (4×u32) + `cpm_fs_oid`/`cpm_oid`/`cpm_paddr` (3×u64).
29const CHECKPOINT_MAPPING_LEN: usize = 40;
30/// Sanity cap on `cpm_count` — a single map block can hold at most
31/// `(block_size - 40) / 40` mappings; cap well above that to reject corruption
32/// without rejecting any legal block.
33const MAX_CPM_COUNT: u32 = 4096;
34
35/// One `checkpoint_mapping_t` entry (ephemeral oid → paddr at a type/subtype).
36#[derive(Debug, Clone, Copy)]
37#[non_exhaustive]
38pub struct CheckpointMapping {
39 pub oid: u64,
40 pub paddr: u64,
41 pub obj_type: u32,
42 pub subtype: u32,
43}
44
45/// Resolution of the live superblock from the checkpoint ring.
46#[derive(Debug, Clone)]
47#[non_exhaustive]
48pub struct LiveCheckpoint {
49 /// Block address of the chosen (highest-valid-xid) NXSB.
50 pub superblock_paddr: u64,
51 /// Its transaction id.
52 pub xid: u64,
53 /// Ephemeral oid → paddr mappings from the checkpoint map.
54 pub mappings: Vec<CheckpointMapping>,
55}
56
57/// Scan the descriptor area and return the live checkpoint.
58///
59/// # Errors
60/// [`crate::ApfsError::NoValidSuperblock`] if no cksum-valid NXSB exists in the
61/// ring — a loud bootstrap failure, never an empty `Ok`.
62pub fn resolve_live_checkpoint<R: std::io::Read + std::io::Seek>(
63 reader: &mut R,
64 bootstrap: &crate::container::NxSuperblock,
65) -> crate::Result<LiveCheckpoint> {
66 // A tree-backed descriptor area needs B-tree resolution (phase P2). Reject
67 // it loudly so a tree oid is never mis-read as a contiguous base address.
68 if bootstrap.xp_desc_is_tree() {
69 return Err(crate::ApfsError::CheckpointTreeUnsupported { area: "descriptor" });
70 }
71
72 // Trust the block size only within the spec range (a corrupt value would
73 // drive every seek to the wrong place / size an over-large buffer).
74 let block_size = bootstrap.block_size;
75 if !(crate::container::NX_MINIMUM_BLOCK_SIZE..=NX_MAXIMUM_BLOCK_SIZE).contains(&block_size) {
76 return Err(crate::ApfsError::FieldOutOfRange {
77 structure: "nx_superblock",
78 field: "nx_block_size",
79 value: u64::from(block_size),
80 cap: u64::from(NX_MAXIMUM_BLOCK_SIZE),
81 });
82 }
83 let block_size = block_size as usize;
84 let mut buf = vec![0u8; block_size];
85
86 // Apple "Mounting an APFS Partition": read the contiguous descriptor area
87 // and choose the cksum-valid NX_SUPERBLOCK with the largest xid.
88 let desc_base = bootstrap.xp_desc_base;
89 let desc_blocks = bootstrap.xp_desc_blocks;
90
91 let mut best: Option<(u64, u64)> = None; // (xid, paddr)
92 let mut checked = 0usize;
93 let mut last_magic = 0u32;
94
95 for i in 0..u64::from(desc_blocks) {
96 let paddr = desc_base + i;
97 if !read_block(reader, paddr, block_size, &mut buf)? {
98 continue; // cov:unreachable: descriptor blocks lie within the image
99 }
100 checked += 1;
101 let Some(hdr) = ObjPhys::parse(&buf) else {
102 continue; // cov:unreachable: buf is block_size >= header length
103 };
104 if hdr.obj_type() != OBJECT_TYPE_NX_SUPERBLOCK {
105 continue;
106 }
107 let magic = crate::bytes::le_u32(&buf, 32);
108 last_magic = magic;
109 if magic != crate::container::NX_MAGIC {
110 continue; // cov:unreachable: an NX_SUPERBLOCK-typed block carries NX_MAGIC
111 }
112 if fletcher64_stored(&buf) != fletcher64_checksum(&buf) {
113 continue;
114 }
115 if best.is_none_or(|(bx, _)| hdr.xid > bx) {
116 best = Some((hdr.xid, paddr));
117 }
118 }
119
120 let Some((xid, superblock_paddr)) = best else {
121 return Err(crate::ApfsError::NoValidSuperblock {
122 checked,
123 last_magic,
124 });
125 };
126
127 // Collect the ephemeral oid→paddr mappings from the checkpoint-map blocks in
128 // the descriptor ring that belong to the live checkpoint (same xid).
129 let mut mappings = Vec::new();
130 for i in 0..u64::from(desc_blocks) {
131 let paddr = desc_base + i;
132 if !read_block(reader, paddr, block_size, &mut buf)? {
133 continue; // cov:unreachable: descriptor blocks lie within the image
134 }
135 let Some(hdr) = ObjPhys::parse(&buf) else {
136 continue; // cov:unreachable: buf is block_size >= header length
137 };
138 if hdr.obj_type() != OBJECT_TYPE_CHECKPOINT_MAP || hdr.xid != xid {
139 continue;
140 }
141 if fletcher64_stored(&buf) != fletcher64_checksum(&buf) {
142 continue; // cov:unreachable: real map blocks carry a valid cksum
143 }
144 let count = crate::bytes::le_u32(&buf, CPM_COUNT_OFF).min(MAX_CPM_COUNT);
145 for m in 0..count as usize {
146 let off = CPM_MAP_OFF + m * CHECKPOINT_MAPPING_LEN;
147 mappings.push(CheckpointMapping {
148 obj_type: crate::bytes::le_u32(&buf, off),
149 subtype: crate::bytes::le_u32(&buf, off + 4),
150 oid: crate::bytes::le_u64(&buf, off + 24),
151 paddr: crate::bytes::le_u64(&buf, off + 32),
152 });
153 }
154 }
155
156 Ok(LiveCheckpoint {
157 superblock_paddr,
158 xid,
159 mappings,
160 })
161}
162
163/// Seek to `paddr * block_size` and fill `buf`. Returns `false` (not an error)
164/// if the block lies past the end of the source — a descriptor index can point
165/// past a truncated image, which is a per-block miss, not a bootstrap failure.
166fn read_block<R: std::io::Read + std::io::Seek>(
167 reader: &mut R,
168 paddr: u64,
169 block_size: usize,
170 buf: &mut [u8],
171) -> crate::Result<bool> {
172 let Some(offset) = paddr.checked_mul(block_size as u64) else {
173 return Ok(false); // cov:unreachable: descriptor paddr*bs cannot overflow u64
174 };
175 reader.seek(std::io::SeekFrom::Start(offset))?;
176 match reader.read_exact(buf) {
177 Ok(()) => Ok(true),
178 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => Ok(false),
179 Err(e) => Err(e.into()),
180 }
181}