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}