Skip to main content

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}