Skip to main content

apfs_core/
object.rs

1//! Object header (`obj_phys_t`) parsing and Fletcher-64 checksum verification.
2//!
3//! Every APFS on-disk object begins with a 32-byte header
4//! (Apple *APFS Reference*, `obj_phys_t`; `MAX_CKSUM_SIZE = 8`):
5//!
6//! | off | size | field        |
7//! |-----|------|--------------|
8//! | 0   | 8    | `o_cksum`    | Fletcher-64 checksum |
9//! | 8   | 8    | `o_oid`      | object identifier    |
10//! | 16  | 8    | `o_xid`      | transaction id       |
11//! | 24  | 4    | `o_type`     | type + storage flags |
12//! | 28  | 4    | `o_subtype`  | subtype              |
13//!
14//! `o_type & OBJECT_TYPE_MASK (0x0000ffff)` selects the object type;
15//! `OBJ_STORAGETYPE_MASK (0xc0000000)` carries physical/ephemeral/virtual flags.
16//!
17//! Object-type constants (complete, from Apple): `INVALID 0x0`, `NX_SUPERBLOCK
18//! 0x1`, `BTREE 0x2`, `BTREE_NODE 0x3`, `SPACEMAN 0x5`, …, `OMAP 0xb`,
19//! `CHECKPOINT_MAP 0xc`, `FS 0xd`, `FSTREE 0xe`, …, `INTEGRITY_META 0x1e`,
20//! `FEXT_TREE 0x1f`, plus the keybag 4CC object types. The authoritative table
21//! lives in [`forensicnomicon`]; this module decodes against it.
22
23/// Size in bytes of the `obj_phys_t` header.
24pub const OBJ_PHYS_LEN: usize = 32;
25
26/// Read the stored Fletcher-64 checksum (`o_cksum`, the first 8 bytes of an
27/// object). Bounds-checked: `0` if the block is shorter than 8 bytes.
28#[must_use]
29pub fn fletcher64_stored(block: &[u8]) -> u64 {
30    crate::bytes::le_u64(block, 0)
31}
32
33/// A parsed object header.
34#[derive(Debug, Clone, Copy)]
35#[non_exhaustive]
36pub struct ObjPhys {
37    /// Stored Fletcher-64 checksum (`o_cksum`).
38    pub cksum: u64,
39    /// Object identifier (`o_oid`) — for a physical object, its block address.
40    pub oid: u64,
41    /// Transaction identifier (`o_xid`).
42    pub xid: u64,
43    /// Raw `o_type` (type + storage/flag bits).
44    pub obj_type_raw: u32,
45    /// `o_subtype`.
46    pub subtype: u32,
47}
48
49impl ObjPhys {
50    /// Parse a header from the start of `block`. Bounds-checked; returns `None`
51    /// if the slice is too short (never panics).
52    ///
53    /// Layout (Apple `obj_phys_t`, little-endian on disk): `o_cksum[8]` @0,
54    /// `o_oid` u64 @8, `o_xid` u64 @16, `o_type` u32 @24, `o_subtype` u32 @28.
55    #[must_use]
56    pub fn parse(block: &[u8]) -> Option<Self> {
57        if block.len() < OBJ_PHYS_LEN {
58            return None;
59        }
60        Some(Self {
61            cksum: crate::bytes::le_u64(block, 0),
62            oid: crate::bytes::le_u64(block, 8),
63            xid: crate::bytes::le_u64(block, 16),
64            obj_type_raw: crate::bytes::le_u32(block, 24),
65            subtype: crate::bytes::le_u32(block, 28),
66        })
67    }
68
69    /// The object type after masking off storage/flag bits
70    /// (`o_type & OBJECT_TYPE_MASK`).
71    #[must_use]
72    pub fn obj_type(&self) -> u16 {
73        (self.obj_type_raw & 0x0000_ffff) as u16
74    }
75}
76
77/// Compute the APFS Fletcher-64 object checksum over `block`, treating the first
78/// 8 bytes (the stored `o_cksum`) as zero.
79///
80/// Apple specifies Fletcher-64; the exact modular arithmetic is taken from the
81/// libfsapfs reverse-engineered spec and **must be validated against real APFS
82/// object test vectors + apfsck** before being trusted (a wrong implementation
83/// would make every block fail verification).
84#[must_use]
85pub fn fletcher64_checksum(block: &[u8]) -> u64 {
86    // APFS Fletcher-64 (Apple names the algorithm; the modular steps follow the
87    // libfsapfs formulation and are validated against a real Apple-stored
88    // o_cksum in core/tests/object.rs):
89    //   - iterate the object as 32-bit little-endian words,
90    //   - treat the 8-byte o_cksum field (the first two words) as zero,
91    //   - accumulate two running sums modulo 0xffffffff,
92    //   - fold into the lower then upper 32 bits.
93    const MOD: u64 = 0xffff_ffff;
94    let mut sum_lo: u64 = 0;
95    let mut sum_hi: u64 = 0;
96
97    // chunks_exact yields only whole 4-byte words; a trailing partial word
98    // (malformed/odd-length input) is ignored — never indexed, never panics.
99    for (i, word) in block.chunks_exact(4).enumerate() {
100        // word is exactly 4 bytes from chunks_exact, so the conversion is total.
101        let v = if i < 2 {
102            0 // the o_cksum field is excluded from its own checksum
103        } else {
104            u64::from(u32::from_le_bytes([word[0], word[1], word[2], word[3]]))
105        };
106        sum_lo = (sum_lo + v) % MOD;
107        sum_hi = (sum_hi + sum_lo) % MOD;
108    }
109
110    let check_lo = MOD - ((sum_lo + sum_hi) % MOD);
111    let check_hi = MOD - ((sum_lo + check_lo) % MOD);
112    (check_hi << 32) | check_lo
113}