Snapshot Metadata
Earlier in this series, we covered how Object Maps facilitate the implementation of point-in-time Snapshots of APFS file systems by preserving File System Tree Nodes from earlier transactions. In that discussion, I outlined the on-disk structure of the Object Map Snapshot Tree and how it can be used to enumerate the transaction identifiers of each Volume Snapshot. Today, we will briefly discuss two other sources of information that store additional metadata about each Snapshot.
Snapshot Metadata Tree
The Snapshot Metadata Tree is a B-Tree whose physical address can be located by reading the apfs_snap_meta_tree_oid field of the Volume Superblock. It stores two types of objects, structured as File System Records.
Snapshot Metadata Records
Snapshot Metadata Records store the bulk of metadata about Volume Snapshots. The key-half is a j_snap_metadata_key structure with an encoded type of APFS_TYPE_SNAP_METADATA.
typedef struct j_snap_metadata_key {
j_key_t hdr; // 0x00
} j_snap_metadata_key_t; // 0x08
hdr: The record’s header. The object identifier in the header is the snapshot’s transaction identifier.
The value-half of the record is a j_snap_metadata_val_t structure and is immediately followed by the UTF-8 encoded name of the snapshot.
typedef struct j_snap_metadata_val {
oid_t extentref_tree_oid; // 0x00
oid_t sblock_oid; // 0x08
uint64_t create_time; // 0x10
uint64_t change_time; // 0x18
uint64_t inum; // 0x20
uint32_t extentref_tree_type; // 0x28
uint32_t flags; // 0x2C
uint16_t name_len; // 0x30
uint8_t name[0]; // 0x32
} j_snap_metadata_val_t;
extentref_tree_oid: The physical object identifier of the B-Tree that stores extent references for the snapshot.sblock_oid: The physical object identifier of a backup of the snapshot’s Volume Superblockcreate_time: The time when the snapshot was createdchange_time: The time that this snapshot was last modifiedinum: The inode number of the snapshot’s directory entry in the Snapshot Metadata Treeextentref_tree_type: The type of the Extent Reference Treeflags: A bit field that contains additional information about a snapshot metadata recordname_len: The length of the null-terminated name that follows this structure, in bytes (including the null terminator)
Snapshot Metadata Record Flags
| Name | Value | Description |
|---|---|---|
| SNAP_META_PENDING_DATALESS | 0x00000001 | This snapshot is dataless, meaning that it does not preserve the file extents |
| SNAP_META_MERGE_IN_PROGRESS | 0x00000002 | The snapshot is in the process of being merged with another |
Snapshot Name Records
Snapshot Name Records are used to map snapshot names to their transaction identifiers. The key-half of the record is a j_snap_name_key_t structure with an encoded type of APFS_TYPE_SNAP_NAME. It is followed by the UTF-8 encoded name of the snapshot.
typedef struct j_snap_name_key {
j_key_t hdr; // 0x00
uint16_t name_len; // 0x08
uint8_t name[0]; // 0x0A
} j_snap_name_key_t;
hdr: The record’s header. The object identifier is always~0ULL.name_len: The length of the null-terminated name, in bytes (including the null terminator)name: The start of the UTF-8 encoded name
The value-half is a j_snap_name_val_t structure.
typedef struct j_snap_name_val {
xid_t snap_xid; // 0x00
} j_snap_name_val_t; // 0x08
snap_xid: The transaction identifier of the snapshot
Snapshot Extended Metadata Object
Each snapshot has a virtual Snapshot Extended Metadata Object in the volume’s Object Map. The virtual object identifier of this object is stored in the apfs_snap_meta_ext_oid field of the Volume Superblock. There are multiple versions of this object whose transaction identifiers correspond to each snapshot.
typedef struct snap_meta_ext_obj_phys {
obj_phys_t smeop_o; // 0x00
snap_meta_ext_t smeop_sme; // 0x20
} snap_meta_ext_obj_phys_t; // 0x48
smeop_o: The object’s headersmeop_sme: The snapshot’s extended metadata
typedef struct snap_meta_ext {
uint32_t sme_version; // 0x00
uint32_t sme_flags; // 0x04
xid_t sme_snap_xid; // 0x08
uuid_t sme_uuid; // 0x10
uint64_t sme_token; // 0x20
} snap_meta_ext_t; // 0x28
sme_version: The version of this structure (currently 1)sme_flags: A bitfield of flags (none are currently defined)sme_snap_xid: The transaction identifier of the snapshotsme_uuid: The unique identifier of the snapshot. When extended metadata does not yet exist, the implementation synthesizes a default UUID by XOR-ing the first eight bytes of the volume’s UUID with the snapshot’s transaction identifier.sme_token: An opaque token (reserved)
Snapshot Deletion
Deleting a snapshot is a multi-step background process that merges physical extent records into an adjacent snapshot before removing the metadata:
-
The snapshot is renamed to
com.apple.apfs.purgatory.<xid>(where<xid>is the transaction identifier in hexadecimal) and a background purgatory cleaner thread is woken. -
The purgatory cleaner locates the adjacent (next) snapshot and begins merging the source snapshot’s Extent Reference Tree into the destination’s tree.
-
During the merge,
SNAP_META_MERGE_IN_PROGRESSis set on both source and destination snapshot metadata records. Extents are merged in batches (up to 2048 per transaction). If a transaction cannot allocate space for the full batch, the batch size is halved until it succeeds. The thread yields between batches to allow other work to proceed. -
The implementation compares the source and destination extentref tree key counts to choose the merge direction. If the destination count is greater than or equal to the source count, it merges the source into the destination (forward merge). If the source count is strictly greater, it swaps roles so the smaller tree is merged into the larger, minimizing B-tree operations.
-
After the merge completes, the source extentref tree is deleted, the snapshot metadata and name records are removed from the Snapshot Metadata Tree, and the Object Map snapshot entry is deleted.
If the merge is interrupted by a crash, SNAP_META_MERGE_IN_PROGRESS signals that it must resume on the next mount. Invalid entries (zero physical block, address exceeding device size) are logged and skipped rather than failing the entire merge.
Snapshot Revert
Reverting a volume to a snapshot replaces the live file system state with the snapshot’s state:
-
If the volume is mounted read-only, the revert is deferred:
apfs_revert_to_xidis set to the target snapshot’s transaction identifier in the Volume Superblock, and the actual revert occurs on the next read-write mount. - On read-write mount (or immediately if already read-write), the revert enters a transaction:
- The snapshot’s Volume Superblock is copied over the current one.
- Physical extent state is reverted.
- The Object Map is reverted via
omap_revert_to_snapshot, which setsom_pending_revert_minandom_pending_revert_maxto skip entries from the reverted range, effectively restoring the OMAP view to the target snapshot. - All snapshots newer than the target are marked for deletion.
- The transaction is committed.
- After the revert, caches (integrity metadata, encryption state) are invalidated and rebuilt. The Reaper handles background cleanup of the deleted snapshot entries.
The key insight is that the revert takes effect immediately through the OMAP’s pending revert mechanism. Background cleanup proceeds asynchronously while the volume is already operating at the reverted state.
Dataless Snapshots
A dataless snapshot has had its physical extent data removed but retains its metadata and Volume Superblock backup. This preserves the snapshot timeline (for audit and restoration purposes) without consuming space for extent data.
Making a snapshot dataless follows the same merge path as full deletion, except that metadata and name records are preserved afterward. The SNAP_META_PENDING_DATALESS flag is set, the purgatory cleaner merges the extentref tree into the adjacent snapshot, and upon completion extentref_tree_oid is set to zero and the flag is cleared.
Constraints on making a snapshot dataless:
- The volume must not be sealed (
APFS_INCOMPAT_SEALED_VOLUMEmust not be set in the snapshot’s superblock) - The live file system must not contain copy-on-write exempt files
- The snapshot must not be currently mounted
Conclusion
Snapshot metadata in APFS is distributed across multiple structures: the Snapshot Metadata Tree stores per-snapshot records and name mappings, the Snapshot Extended Metadata Object provides UUIDs and versioning, and the Extent Reference Trees track which physical extents each snapshot owns. Deletion and revert are multi-phase processes that leverage the Reaper and the Object Map’s revert mechanism to maintain consistency across transactions.
Find an issue or technical inaccuracy in this post? Please file an issue so that it may be corrected.