Sealed Volumes
With the release of macOS 11, Apple added a security feature to APFS called sealed volumes. Sealed volumes can be used to cryptographically verify the contents of the read-only system volume as an additional layer of protection against rootkits and other malware that may attempt to replace critical components of the operating system. Sealed volumes have subtle differences from some of the properties of file systems that we’ve discussed so far.
Identifying a Sealed Volume
Sealed volumes can be identified by checking for the APFS_INCOMPAT_SEALED_VOLUME flag in the apfs_incompatible_features field of their Volume Superblock. In addition, the apfs_integrity_meta_oid and apfs_fext_tree_oid fields must have non-zero values.
An Integrity Metadata Object stores information about the sealed volume. This is a virtual object that is owned by the volume’s Object Map and whose object identifier can be found in the apfs_integrity_meta_oid field of the Volume Superblock. On disk, it is stored as an integrity_meta_phys_t structure.
typedef struct integrity_meta_phys {
obj_phys_t im_o; // 0x00
uint32_t im_version; // 0x20
uint32_t im_flags; // 0x24
apfs_hash_type_t im_hash_type; // 0x28
uint32_t im_root_hash_offset; // 0x2C
xid_t im_broken_xid; // 0x30
uint64_t im_reserved[9]; // 0x38
} integrity_meta_phys_t; // 0x80
im_o: The object’s headerim_version: The version of the data structureim_flags: The configuration flagsim_hash_type: The hash algorithm that is usedim_root_hash_offset: The offset (in bytes) of the root hash relative to the start of the objectim_broken_xid: The identifier of the transaction whose modification broke the seal (zero if the seal is intact)im_reserved: reserved (only in version 2 or above)
Integrity Metadata Flags
| Name | Value | Description |
|---|---|---|
| APFS_SEAL_BROKEN | 0x00000001 | The volume was modified after being sealed, breaking its seal |
Hash Types
| Name | Value | Digest Size | Description |
|---|---|---|---|
| APFS_HASH_INVALID | 0 | n/a | An invalid hash algorithm |
| APFS_HASH_SHA256 | 0x1 | 32 bytes | SHA-256 (default) |
| APFS_HASH_SHA512_256_DEPRECATED | 0x2 | 32 bytes | SHA-512/256 (deprecated; use type 0x5 instead) |
| APFS_HASH_SHA384 | 0x3 | 48 bytes | SHA-384 |
| APFS_HASH_SHA512 | 0x4 | 64 bytes | SHA-512 |
| APFS_HASH_SHA512_256 | 0x5 | 32 bytes | SHA-512/256 (replacement for deprecated type 0x2) |
| APFS_HASH_SHA3_256 | 0x6 | 32 bytes | SHA3-256 |
| APFS_HASH_SHA3_384 | 0x7 | 48 bytes | SHA3-384 |
| APFS_HASH_SHA3_512 | 0x8 | 64 bytes | SHA3-512 |
Hash type 0x2 (SHA512_256) is deprecated and rejected during validation. The validation check (hash_type & 0xFD) != 0 rejects both type 0 (invalid) and type 2 (deprecated).
File System Tree
Sealed Volumes verify integrity by hashing the contents of their File System Trees. This hashing necessitates some slight differences to the B-Tree. These modified B-Trees can be identified by the BTREE_HASHED and BTREE_NOHEADER flags being set in their B-Tree Info.
In standard B-Trees, non-leaf nodes store the object identifier of their children in the value-half of their entries. “Hashed” B-Trees instead use btn_index_node_val_t structures for this purpose, which store the cryptographic hash of the child node’s contents along with its identifier. Hashed nodes are also stored as headerless objects, with their 32-byte header being zeroed out.
#define BTREE_NODE_HASH_SIZE_MAX 64
typedef struct btn_index_node_val {
oid_t binv_child_oid; // 0x00
uint8_t binv_child_hash[BTREE_NODE_HASH_SIZE_MAX]; // 0x08
} btn_index_node_val_t; // 0x48
binv_child_oid: The object identifier of the child nodebinv_child_hash: The hash of the child node
Data Stream Extents
As we discussed in an earlier post, Data Streams store their extents as file system records in the File System Tree. Sealed Volumes store extents in a separate File Extent Tree, whose physical object identifier is stored in the apfs_fext_tree_oid of the Volume Superblock.
The key-half of the File Extent Tree entries are fext_tree_key_t structures and are sorted first by private_id and then by logical_addr.
typedef struct fext_tree_key {
uint64_t private_id; // 0x00
uint64_t logical_addr; // 0x08
} fext_tree_key_t; // 0x10
private_id: The object identifier of the filelogical_addr: The offset (in bytes) within the file’s data for the data stored in this extent
The value-half takes the form of a fext_tree_val_t structure. Its fields are interpreted in the same way as the j_file_extent_val fields. There is no crypto_id because sealed system volumes are never encrypted.
typedef struct fext_tree_val {
uint64_t len_and_flags; // 0x00
uint64_t phys_block_num; // 0x08
} fext_tree_val_t; // 0x10
len_and_flags: A bit field that contains the length of the extent and its flagsphys_block_num: The starting physical block address of the extent
Root Hash Verification
At mount time, the sealed volume’s integrity is verified by comparing the on-disk root hash against a known-good value supplied by the boot chain:
- The system checks whether root hash authentication is required based on the volume role and system configuration.
- If the seal is broken (
APFS_SEAL_BROKENis set inim_flags), the mount is rejected withEAUTH(error 80) when authentication is required. - The root hash payload contains block-size-specific root hashes. The appropriate hash is selected based on the device’s block size: 8 KiB blocks use offset +80, 16 KiB blocks use offset +144, and all other sizes (including 4 KiB) use offset +16.
- The selected hash is compared against the on-disk root hash stored at
im_root_hash_offsetwithin the integrity metadata object. This offset must be at least 0x30 (48 bytes) and is typically set to 0x80 (128 bytes). - If the hashes match, the volume is verified. If they do not match and authentication is required, the system boots to recovery mode.
Because the hashed B-Tree functions as a Merkle tree, the root hash transitively verifies every node in the File System Tree. Modifying any leaf node changes its hash, which propagates up through every ancestor node to the root.
Seal Breaking
When a sealed volume is modified (for example, during a system update), the seal is broken:
APFS_SEAL_BROKEN(bit 0) is set inim_flags.- If
im_versionis 2 or above, the current transaction identifier is stored inim_broken_xid. - The volume’s verification flag is cleared.
To restore a broken seal, the implementation verifies that im_version >= 2 and that the file system root tree has not been modified since the seal was broken (the root tree’s maximum transaction identifier must be less than im_broken_xid). If both conditions hold, APFS_SEAL_BROKEN is cleared and im_broken_xid is zeroed.
File Info Records
Sealed volumes also use file info records (type APFS_TYPE_FILE_INFO) to store per-extent data hashes and attribution tags. These records enable verification of individual file data blocks without re-hashing the entire tree.
typedef struct j_file_info_key {
j_key_t hdr; // 0x00
uint64_t info_and_lba; // 0x08
} j_file_info_key_t; // 0x10
hdr: The record’s header (typeAPFS_TYPE_FILE_INFO)info_and_lba: The physical block address (lower 56 bits) and record type (upper 8 bits)
Two record types exist:
| Name | Value | Description |
|---|---|---|
| APFS_FILE_INFO_DATA_HASH | 1 | A hash of the file’s data at a specific extent |
| APFS_FILE_INFO_ATTRIBUTION_TAG | 2 | A code-signing attribution tag |
Data hash values store the hash of a segment of file data:
typedef struct j_file_data_hash_val {
uint16_t hashed_len; // 0x00
uint8_t hash_size; // 0x02
uint8_t hash[]; // 0x03
} j_file_data_hash_val_t;
hashed_len: The length of the hashed data segment, in blockshash_size: The length of the hash in bytes (must match the digest size forim_hash_type)hash: The hash data
Conclusion
Sealed Volumes in APFS provide an extra layer of security by allowing macOS to verify its system volume cryptographically. The Merkle-tree-like hashed B-Tree structure, combined with root hash verification at mount time and per-extent data hashes, ensures that any modification to the system volume is detectable. Understanding seal breaking, hash verification, and the additional file info records is essential for analyzing sealed system volumes.
Find an issue or technical inaccuracy in this post? Please file an issue so that it may be corrected.