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}