Skip to main content

apfs_core/
container.rs

1//! Container superblock (`nx_superblock_t`, magic `NX_MAGIC = 'BSXN'` →
2//! "NXSB" → LE `0x4253584E`) and container geometry.
3//!
4//! The NXSB (Apple *APFS Reference*, `nx_superblock_t`) names the block size
5//! (`nx_block_size`, default 4096, max 65536), block count, feature/incompatible
6//! flags, container UUID, the spaceman/omap/reaper oids, the checkpoint
7//! descriptor + data areas (`nx_xp_desc_base/len`, `nx_xp_data_base/len`), and
8//! the volume oids (`nx_fs_oid[]`). Block 0 holds *a* copy, but the **live**
9//! superblock is found via the checkpoint ring ([`crate::checkpoint`]).
10//!
11//! The EFI jumpstart (`nx_efi_jumpstart`, magic `NX_EFI_JUMPSTART_MAGIC =
12//! 'RDSJ'` → "JSDR") locates the bootable EFI driver, parsed here for
13//! completeness.
14
15/// Container superblock magic `NX_MAGIC` ('BSXN', "NXSB" in a hex dump).
16pub const NX_MAGIC: u32 = 0x4253_584E;
17
18/// Smallest / default / largest block size and minimum container size (Apple).
19/// `NX_INCOMPAT_FUSION` — the `nx_incompatible_features` bit set on a Fusion
20/// (SSD+HDD) container (Apple *APFS Reference*). A reader that does not implement
21/// tier-aware address translation must reject such a container rather than
22/// mis-read physical addresses (see [`crate::fusion`]).
23pub const NX_INCOMPAT_FUSION: u64 = 0x100;
24
25pub const NX_MINIMUM_BLOCK_SIZE: u32 = 4096;
26pub const NX_DEFAULT_BLOCK_SIZE: u32 = 4096;
27pub const NX_MAXIMUM_BLOCK_SIZE: u32 = 65536;
28pub const NX_MINIMUM_CONTAINER_SIZE: u64 = 1_048_576;
29
30/// `NX_MAX_FILE_SYSTEMS` (Apple) — the hard cap on volumes per container, used
31/// as the sanity bound on `nx_max_file_systems` (allocation-bomb defense).
32pub const NX_MAX_FILE_SYSTEMS: u32 = 100;
33
34/// The high bit of `nx_xp_{desc,data}_blocks` is a flag (Apple): when set, the
35/// corresponding checkpoint area is stored as a B-tree rather than contiguously.
36const XP_BLOCKS_TREE_FLAG: u32 = 0x8000_0000;
37
38// Verified field offsets within `nx_superblock_t` (little-endian on disk),
39// after the 32-byte `obj_phys_t nx_o` header (Apple *APFS Reference*):
40const OFF_MAGIC: usize = 32; // nx_magic           u32
41const OFF_BLOCK_SIZE: usize = 36; // nx_block_size  u32
42const OFF_BLOCK_COUNT: usize = 40; // nx_block_count u64
43const OFF_INCOMPAT_FEATURES: usize = 64; // nx_incompatible_features u64
44const OFF_UUID: usize = 72; // nx_uuid            uuid_t (16)
45const OFF_SPACEMAN_OID: usize = 152; // nx_spaceman_oid oid (u64, ephemeral)
46const OFF_REAPER_OID: usize = 168; // nx_reaper_oid    oid (u64, ephemeral)
47const OFF_XP_DESC_BLOCKS: usize = 104; // nx_xp_desc_blocks u32
48const OFF_XP_DATA_BLOCKS: usize = 108; // nx_xp_data_blocks u32
49const OFF_XP_DESC_BASE: usize = 112; // nx_xp_desc_base paddr (u64)
50const OFF_XP_DATA_BASE: usize = 120; // nx_xp_data_base paddr (u64)
51const OFF_OMAP_OID: usize = 160; // nx_omap_oid     oid (u64)
52const OFF_MAX_FILE_SYSTEMS: usize = 180; // nx_max_file_systems u32
53const OFF_FS_OID: usize = 184; // nx_fs_oid[NX_MAX_FILE_SYSTEMS] oid[] (u64 each)
54
55/// A parsed container superblock (subset; `#[non_exhaustive]` for additive growth).
56#[derive(Debug, Clone)]
57#[non_exhaustive]
58pub struct NxSuperblock {
59    /// Transaction id of this superblock (`nx_o.o_xid`).
60    pub xid: u64,
61    /// Container UUID (`nx_uuid`).
62    pub uuid: [u8; 16],
63    /// `nx_block_size` — the authoritative block size (never assumed).
64    pub block_size: u32,
65    /// `nx_block_count`.
66    pub block_count: u64,
67    /// `nx_incompatible_features` — feature bits a reader must understand to
68    /// mount safely (e.g. [`NX_INCOMPAT_FUSION`]); an unrecognised bit means the
69    /// image cannot be read correctly.
70    pub incompatible_features: u64,
71    /// Checkpoint descriptor-area base (`nx_xp_desc_base`) — a block address
72    /// when contiguous, or a B-tree oid when [`Self::xp_desc_is_tree`].
73    pub xp_desc_base: u64,
74    /// Checkpoint descriptor-area length in blocks (`nx_xp_desc_blocks`, flag
75    /// bit already masked off).
76    pub xp_desc_blocks: u32,
77    /// Checkpoint data-area base (`nx_xp_data_base`).
78    pub xp_data_base: u64,
79    /// Checkpoint data-area length in blocks (`nx_xp_data_blocks`, flag bit masked).
80    pub xp_data_blocks: u32,
81    /// Container object-map oid (`nx_omap_oid`).
82    pub omap_oid: u64,
83    /// Space-manager ephemeral oid (`nx_spaceman_oid`) — resolve to a paddr via
84    /// the checkpoint map.
85    pub spaceman_oid: u64,
86    /// Reaper ephemeral oid (`nx_reaper_oid`) — resolve to a paddr via the
87    /// checkpoint map.
88    pub reaper_oid: u64,
89    /// Volume oids (`nx_fs_oid[]`), one per APSB volume (trailing zeros trimmed).
90    pub fs_oids: Vec<u64>,
91    /// Whether the descriptor area is a B-tree (high bit of `nx_xp_desc_blocks`).
92    desc_is_tree: bool,
93    /// Whether the data area is a B-tree (high bit of `nx_xp_data_blocks`).
94    data_is_tree: bool,
95}
96
97impl NxSuperblock {
98    /// Parse and validate an NXSB from a block (checks magic + checksum).
99    ///
100    /// # Errors
101    /// [`crate::ApfsError::NoValidSuperblock`] on a short block or bad magic
102    /// (carrying the offending magic value);
103    /// [`crate::ApfsError::ChecksumMismatch`] on a Fletcher-64 failure
104    /// (carrying both the stored and computed checksums).
105    pub fn parse(block: &[u8]) -> crate::Result<Self> {
106        // A block too short to hold even the magic field has no readable
107        // superblock — surface that as a loud bootstrap failure, not Ok(empty).
108        if block.len() < OFF_MAGIC + 4 {
109            return Err(crate::ApfsError::NoValidSuperblock {
110                checked: 1,
111                last_magic: 0,
112            });
113        }
114
115        // 1. Magic gate (Apple mounting step 3): nx_magic == NX_MAGIC.
116        let magic = crate::bytes::le_u32(block, OFF_MAGIC);
117        if magic != NX_MAGIC {
118            return Err(crate::ApfsError::NoValidSuperblock {
119                checked: 1,
120                last_magic: magic,
121            });
122        }
123
124        // 2. Checksum gate (Apple mounting step 2): verify Fletcher-64 before
125        //    trusting any geometry field.
126        let stored = crate::object::fletcher64_stored(block);
127        let computed = crate::object::fletcher64_checksum(block);
128        if stored != computed {
129            let oid = crate::bytes::le_u64(block, 8);
130            return Err(crate::ApfsError::ChecksumMismatch {
131                block: oid,
132                stored,
133                computed,
134            });
135        }
136
137        // 3. Geometry — only now are the fields trustworthy.
138        let xid = crate::bytes::le_u64(block, 16);
139        let uuid = crate::bytes::arr::<16>(block, OFF_UUID);
140        let block_size = crate::bytes::le_u32(block, OFF_BLOCK_SIZE);
141        let block_count = crate::bytes::le_u64(block, OFF_BLOCK_COUNT);
142        let incompatible_features = crate::bytes::le_u64(block, OFF_INCOMPAT_FEATURES);
143
144        let desc_blocks_raw = crate::bytes::le_u32(block, OFF_XP_DESC_BLOCKS);
145        let data_blocks_raw = crate::bytes::le_u32(block, OFF_XP_DATA_BLOCKS);
146        let desc_is_tree = desc_blocks_raw & XP_BLOCKS_TREE_FLAG != 0;
147        let data_is_tree = data_blocks_raw & XP_BLOCKS_TREE_FLAG != 0;
148
149        let omap_oid = crate::bytes::le_u64(block, OFF_OMAP_OID);
150        let spaceman_oid = crate::bytes::le_u64(block, OFF_SPACEMAN_OID);
151        let reaper_oid = crate::bytes::le_u64(block, OFF_REAPER_OID);
152
153        // Cap nx_max_file_systems before reading the fs_oid array (Apple bounds
154        // it at NX_MAX_FILE_SYSTEMS; a larger value is corruption — clamp to the
155        // spec maximum so a hostile image can't drive an over-read loop).
156        let max_fs = crate::bytes::le_u32(block, OFF_MAX_FILE_SYSTEMS).min(NX_MAX_FILE_SYSTEMS);
157        let mut fs_oids = Vec::new();
158        for i in 0..max_fs as usize {
159            let oid = crate::bytes::le_u64(block, OFF_FS_OID + i * 8);
160            if oid != 0 {
161                fs_oids.push(oid);
162            }
163        }
164
165        Ok(Self {
166            xid,
167            uuid,
168            block_size,
169            block_count,
170            incompatible_features,
171            xp_desc_base: crate::bytes::le_u64(block, OFF_XP_DESC_BASE),
172            xp_desc_blocks: desc_blocks_raw & !XP_BLOCKS_TREE_FLAG,
173            xp_data_base: crate::bytes::le_u64(block, OFF_XP_DATA_BASE),
174            xp_data_blocks: data_blocks_raw & !XP_BLOCKS_TREE_FLAG,
175            omap_oid,
176            spaceman_oid,
177            reaper_oid,
178            fs_oids,
179            desc_is_tree,
180            data_is_tree,
181        })
182    }
183
184    /// Whether the checkpoint descriptor area is stored as a B-tree (high bit of
185    /// `nx_xp_desc_blocks` set) rather than a contiguous run. A contiguous area
186    /// can be walked directly; a tree-backed one needs B-tree resolution (P2).
187    #[must_use]
188    pub fn xp_desc_is_tree(&self) -> bool {
189        self.desc_is_tree
190    }
191
192    /// Whether the checkpoint data area is stored as a B-tree.
193    #[must_use]
194    pub fn xp_data_is_tree(&self) -> bool {
195        self.data_is_tree
196    }
197}