apfs_core/sealed.rs
1//! Sealed / signed system volume: integrity metadata and file-info records —
2//! **parse + accessors only** (hash recomputation + seal validation live in the
3//! analyzer, `apfs-forensic::sealed`).
4//!
5//! On a sealed volume (macOS Signed System Volume), an
6//! `integrity_meta_phys_t` object (type `OBJECT_TYPE_INTEGRITY_META 0x1e`)
7//! anchors a Merkle tree over the volume's content. Apple *APFS Reference*,
8//! `integrity_meta_phys_t`: `im_version`, `im_flags`, `im_hash_type`
9//! (`apfs_hash_type_t`, e.g. SHA-256), `im_root_hash_offset`, **`im_broken_xid`**
10//! (non-zero ⇒ the seal was broken at that transaction), `im_reserved[9]`.
11//! Per-file hashes live in `APFS_TYPE_FILE_INFO 13` records (`j_file_info_val_t`)
12//! and a file-extent tree (`fext_tree_key_t`/`fext_tree_val_t`, type
13//! `OBJECT_TYPE_FEXT_TREE 0x1f`).
14//!
15//! This module decodes those structures and exposes accessors (root hash, hash
16//! type, `im_broken_xid`, per-file hashes). It does **not** recompute or compare
17//! hashes — that is a forensic judgment (and the exact canonicalization is the
18//! least-documented, highest-FP-risk area), so it lives in the analyzer and is
19//! validated only against a real SSV image + apfsck.
20
21/// Parsed integrity metadata.
22#[derive(Debug, Clone)]
23#[non_exhaustive]
24pub struct IntegrityMeta {
25 pub version: u32,
26 pub flags: u32,
27 pub hash_type: u32,
28 /// Non-zero ⇒ the seal is recorded as broken at this transaction id.
29 pub broken_xid: u64,
30}
31
32// `integrity_meta_phys_t` field offsets (after the 32-byte obj_phys header).
33const OFF_IM_VERSION: usize = 32; // u32
34const OFF_IM_FLAGS: usize = 36; // u32
35const OFF_IM_HASH_TYPE: usize = 40; // u32 (apfs_hash_type_t)
36const OFF_IM_BROKEN_XID: usize = 48; // xid_t (u64)
37const INTEGRITY_META_MIN_LEN: usize = OFF_IM_BROKEN_XID + 8;
38
39impl IntegrityMeta {
40 /// Parse an `integrity_meta_phys_t` (parse only — no hash recomputation,
41 /// which is a forensic judgment in `apfs-forensic::sealed`).
42 ///
43 /// # Errors
44 /// [`crate::ApfsError::ChecksumMismatch`] on a Fletcher-64 failure (checksum-
45 /// before-trust); [`crate::ApfsError::FieldOutOfRange`] if the block is too
46 /// short to hold the structure.
47 pub fn parse(block: &[u8]) -> crate::Result<Self> {
48 if block.len() < INTEGRITY_META_MIN_LEN {
49 return Err(crate::ApfsError::FieldOutOfRange {
50 structure: "integrity_meta_phys",
51 field: "block.len",
52 value: block.len() as u64,
53 cap: INTEGRITY_META_MIN_LEN as u64,
54 });
55 }
56 let stored = crate::object::fletcher64_stored(block);
57 let computed = crate::object::fletcher64_checksum(block);
58 if stored != computed {
59 return Err(crate::ApfsError::ChecksumMismatch {
60 block: crate::bytes::le_u64(block, 8),
61 stored,
62 computed,
63 });
64 }
65 Ok(Self {
66 version: crate::bytes::le_u32(block, OFF_IM_VERSION),
67 flags: crate::bytes::le_u32(block, OFF_IM_FLAGS),
68 hash_type: crate::bytes::le_u32(block, OFF_IM_HASH_TYPE),
69 broken_xid: crate::bytes::le_u64(block, OFF_IM_BROKEN_XID),
70 })
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 /// Build a checksum-valid `integrity_meta_phys_t` block: fields after the
79 /// 32-byte `obj_phys` header — `im_version`@32, `im_flags`@36, `im_hash_type`@40,
80 /// `im_root_hash_offset`@44, `im_broken_xid`@48 (u64).
81 fn integrity_block(version: u32, flags: u32, hash_type: u32, broken_xid: u64) -> Vec<u8> {
82 let mut b = vec![0u8; 4096];
83 b[32..36].copy_from_slice(&version.to_le_bytes());
84 b[36..40].copy_from_slice(&flags.to_le_bytes());
85 b[40..44].copy_from_slice(&hash_type.to_le_bytes());
86 b[48..56].copy_from_slice(&broken_xid.to_le_bytes());
87 let cks = crate::object::fletcher64_checksum(&b);
88 b[0..8].copy_from_slice(&cks.to_le_bytes());
89 b
90 }
91
92 #[test]
93 fn parses_fields_including_broken_xid() {
94 // im_hash_type 1 = SHA-256 (APFS_HASH_SHA256); seal broken at xid 42.
95 let block = integrity_block(1, 0, 1, 42);
96 let im = IntegrityMeta::parse(&block).expect("parse integrity_meta");
97 assert_eq!(im.version, 1);
98 assert_eq!(im.hash_type, 1);
99 assert_eq!(im.broken_xid, 42);
100 }
101
102 #[test]
103 fn unbroken_seal_has_zero_broken_xid() {
104 let block = integrity_block(1, 0, 1, 0);
105 let im = IntegrityMeta::parse(&block).expect("parse");
106 assert_eq!(im.broken_xid, 0);
107 }
108
109 #[test]
110 fn rejects_corrupted_block() {
111 // A bad Fletcher-64 must fail loud (checksum-before-trust), never parse.
112 let mut block = integrity_block(1, 0, 1, 0);
113 block[100] ^= 0xff;
114 assert!(IntegrityMeta::parse(&block).is_err());
115 }
116
117 #[test]
118 fn rejects_a_block_too_short_for_the_structure() {
119 // A block shorter than INTEGRITY_META_MIN_LEN is a loud FieldOutOfRange
120 // carrying the offending length — never a panic on the field reads.
121 let got = IntegrityMeta::parse(&[0u8; 16]);
122 let Err(crate::ApfsError::FieldOutOfRange { field, value, .. }) = got else {
123 unreachable!("a short block must be FieldOutOfRange, got {got:?}") // cov:unreachable
124 };
125 assert_eq!(field, "block.len");
126 assert_eq!(value, 16);
127 }
128}