Skip to main content

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}