Object Maps
Earlier in this series, we discussed APFS Containers and how they address physical objects via a fixed block size. This was followed by a discussion on enumerating Checkpoint Maps to locate ephemeral objects. The last remaining kind of objects that we need to know how to find are virtual objects. Today, we will discuss an essential specialization of B-Trees, the Object Map (OMAP), and their critical role in managing these virtual objects in APFS.
Object Maps perform two essential roles in APFS. They facilitate the address translation needed to locate virtual objects on disk and provide snapshot capabilities that can instantly roll back the set of virtual objects to an earlier point in time. The Container and each Volume maintain their own independent OMAPs. Each OMAP has its own virtual address space, so when dereferencing a virtual object, it is essential to understand how that object is used to know which object map to query.
On-Disk Structures
OMAP Objects have a relatively simple on-disk structure. Along with some minor metadata, the primary use of the OMAP Object is to store the physical address of its tree. Optionally, they also store an address to a Snapshot Tree. Both trees are structured as B-Tree Objects.
Incidentally, there are indications that APFS was intended to support more than one type of tree structure. Until recently, Apple’s apfs.kext contained references to an undocumented “M-Tree” type, which appeared to be designed to be used in place of B-Trees in some instances. M-Trees are never mentioned in Apple’s APFS File System Reference, I have never seen them on disk, and all references of M-Trees were removed from the macOS 13 apfs.kext.
typedef struct omap_phys {
obj_phys_t om_o; // 0x00
uint32_t om_flags; // 0x20
uint32_t om_snap_count; // 0x24
uint32_t om_tree_type; // 0x28
uint32_t om_snapshot_tree_type; // 0x2C
oid_t om_tree_oid; // 0x30
oid_t om_snapshot_tree_oid; // 0x38
xid_t om_most_recent_snap; // 0x40
xid_t om_pending_revert_min; // 0x48
xid_t om_pending_revert_max; // 0x50
oid_t om_min_oid; // 0x58
} omap_phys_t; // 0x60
om_o: The object headerom_flags: OMAP flags (see below)om_snap_count: The number of snapshotsom_tree_type: The storage type of the OMAP tree. This isOBJECT_TYPE_BTREEcombined with a storage flag, either physical (0x40000002) or ephemeral (0x80000002); in practice it is physical for both the container and volume OMAPsom_snapshot_tree_type: The storage type of the Snapshot tree, either physical (0x40000002) or ephemeral (0x80000002). Newly created snapshot trees are always physicalom_tree_oid: The physical object identifier of the omap’s B-Treeom_snapshot_tree_oid: The physical object identifier of the snapshot’s B-Tree or zero if there is noneom_most_recent_snap: The transaction identifier of the latest snapshotom_pending_revert_min: The earliest transaction identifier for an in-progress revertom_pending_revert_max: The latest transaction identifier for an in-progress revertom_min_oid: The minimum valid object identifier for lookups. Any lookup below this value fails immediately. During reaping, entries below this watermark are unconditionally removed.
NOTE: Apple’s APFS File System Reference incorrectly lists the om_tree_oid and om_snapshot_tree_oid members as _virtual object identifiers. They are not virtual; each is a physical object identifier when its *_tree_type is physical, or an ephemeral object identifier when it is ephemeral.
Object Map Flags
| Name | Value | Description |
|---|---|---|
| OMAP_MANUALLY_MANAGED | 0x00000001 | Does not support snapshots |
| OMAP_ENCRYPTING | 0x00000002 | Encryption in progress |
| OMAP_DECRYPTING | 0x00000004 | Decryption in progress |
| OMAP_KEYROLLING | 0x00000008 | Encryption key change in progress |
| OMAP_CRYPTO_GENERATION | 0x00000010 | Tracks the current encryption configuration generation |
| OMAP_CONVERSION_IN_PROGRESS | 0x00000020 | A crypto conversion operation is actively in progress |
Object Map B-Tree
An OMAP tree is a specialized B-Tree type that maps fixed-size omap_key_t and omap_val_t key/value pairs. There can be more than one referenced object with the same virtual object identifier (oid) stored in the tree. Transaction identifiers (xid) are stored to identify these versions.
Keys are sorted in ascending order, first by oid and then xid. Values contain the size and physical address of the mapped object. We will discuss APFS encryption in the future, but for now, it is sufficient to note that omap_val_t marks encrypted objects via the OMAP_VAL_ENCRYPTED bit-flag.
typedef struct omap_key {
oid_t ok_oid; // 0x00
xid_t ok_xid; // 0x08
} omap_key_t; // 0x10
ok_oid: The mapped object’s virtual object identifierok_xid: The mapped object’s transaction identifier
typedef struct omap_val {
uint32_t ov_flags; // 0x00
uint32_t ov_size; // 0x04
paddr_t ov_paddr; // 0x08
} omap_val_t; // 0x10
ov_flags: OMAP value flags (see below)ov_size: Size of the mapped object (in bytes)ov_paddr: The physical address of the start of the mapped object
Object Map Value Flags
| Name | Value | Description |
|---|---|---|
| OMAP_VAL_DELETED | 0x00000001 | Object mapping has been removed from the map and this is a placeholder |
| OMAP_VAL_SAVED | 0x00000002 | This object mapping shouldnʼt be replaced when the object is updated. (currently unused) |
| OMAP_VAL_ENCRYPTED | 0x00000004 | The mapped object is encrypted |
| OMAP_VAL_NOHEADER | 0x00000008 | The mapped object has a zero’d object header |
| OMAP_VAL_CRYPTO_GENERATION | 0x00000010 | Encryption change marker |
Object Map Snapshot Tree
Volume OMAPs (those without OMAP_MANUALLY_MANAGED set) maintain an entry for each of their snapshots in a Snapshot Tree. The container’s OMAP is manually managed and does not support snapshots. These trees map xid_t transaction identifiers to omap_snapshot_t values. Other than the deletion state of a snapshot, there is very little information to be gained from enumerating this tree. Volumes maintain additional trees that store more detailed metadata about snapshots. We will discuss those trees in the future.
typedef struct omap_snapshot {
uint32_t oms_flags; // 0x00
uint32_t oms_pad; // 0x04
oid_t oms_oid; // 0x08
} omap_snapshot_t; // 0x10
oms_flags- OMAP Snapshot Flags (see below)oms_pad- paddingoms_oid- reserved and unused
| Name | Value | Description |
|---|---|---|
| OMAP_SNAPSHOT_DELETED | 0x00000001 | The snapshot has been deleted |
| OMAP_SNAPSHOT_REVERTED | 0x00000002 | The snapshot has been deleted as part of a revert |
Parsing Object Maps
Once you understand the structure of the OMAP key/value pairs, the process of parsing an OMAP is the same as parsing other B-Trees. When looking up a virtual object from the active filesystem state, choose the key with the highest xid available. If the selected value has the OMAP_VAL_DELETED flag set in ov_flags, the mapping is a tombstone and no valid object exists. If parsing the state from a snapshot, ignore all keys whose xid is greater than the xid of the snapshot in question.
If om_pending_revert_min is nonzero (indicating a revert is in progress), skip any entry whose xid falls within [om_pending_revert_min, om_pending_revert_max] and use the next-best match. This is how APFS makes the revert take effect immediately while background cleanup proceeds.
Snapshot Lifecycle
The OMAP’s snapshot capabilities are what make APFS snapshots nearly instantaneous. Rather than copying data, snapshots simply freeze a transaction boundary. All object versions at or before the snapshot’s xid are preserved, and any subsequent modifications create new entries via copy-on-write without disturbing the snapshot’s view.
Snapshot Creation
Creating a snapshot is lightweight:
- If
om_snapshot_tree_oidis zero (no snapshot tree exists yet), a new physical B-Tree is created for the snapshot tree. - A new entry is inserted into the snapshot tree with the snapshot’s transaction identifier as the key and a zeroed
omap_snapshot_tas the value. om_most_recent_snapis set to the new snapshot’s transaction identifier.om_snap_countis incremented.
From this point forward, any modification to a virtual object that was last written at or before the snapshot’s xid triggers a copy-on-write: the old mapping remains in the OMAP tree (accessible to the snapshot), and a new mapping is created at the current xid pointing to the modified copy.
Snapshot Deletion
Deleting a snapshot is more involved because it may require freeing physical blocks that were only preserved for that snapshot:
- The snapshot entry in the snapshot tree has
OMAP_SNAPSHOT_DELETEDset in its flags. - The OMAP is added to the Reaper’s work list for background cleanup.
The Reaper processes the deletion in two phases:
-
Phase 1 (OMAP_REAP_PHASE_MAP_TREE): The Reaper iterates the OMAP B-Tree and removes entries whose physical blocks are only referenced by the deleted snapshot. These blocks are freed back to the Space Manager. Entries below
om_min_oidare also unconditionally removed. The Reaper processes entries in batches across multiple transactions, yielding between batches to allow other work to proceed. -
Phase 2 (OMAP_REAP_PHASE_SNAPSHOT_TREE): The Reaper removes the snapshot’s entry from the snapshot tree and decrements
om_snap_count. Ifom_snap_countreaches zero, the snapshot tree is deleted andom_snapshot_tree_oidis set to zero.
Snapshot Revert
Reverting to a snapshot restores the OMAP’s view to an earlier point in time:
- A new snapshot is created at the current transaction identifier to preserve the current state (as a safety net).
om_pending_revert_minis set totarget_snapshot_xid + 1.om_pending_revert_maxis set to the current transaction identifier.- All snapshots between the target and the current transaction are marked with both
OMAP_SNAPSHOT_DELETEDandOMAP_SNAPSHOT_REVERTED.
The revert takes effect immediately: all OMAP lookups skip entries with transaction identifiers in the range [om_pending_revert_min, om_pending_revert_max], which effectively restores the view to the target snapshot. The Reaper cleans up the reverted entries in the background, progressively advancing om_pending_revert_min as each snapshot in the range is processed. When om_pending_revert_min reaches om_pending_revert_max, both fields are set to zero, completing the revert.
Object Map Eviction
The container can evict OMAP entries from a physical address range to reclaim disk space. This is used during defragmentation, tier migration (on Fusion Drives), and container resize operations. The eviction process:
- Iterates the OMAP B-Tree looking for entries whose
ov_paddrfalls within the target range. - For each matching entry, reads the object from its current physical address.
- Writes the object to a new location allocated by the Space Manager.
- Updates the OMAP entry to point to the new location.
For objects that have a snapshot-protected prior version at the same physical address, eviction creates a new version in the object map pointing to the new location without disturbing the snapshot’s view of the old address. Eviction is cancellable: if interrupted (for example, due to unmount), the operation stops and returns ECANCELED.
The container’s NX Superblock tracks eviction state via nx_evict_mapping_tree_oid, which points to an evict-mapping tree that records source-to-destination block relocations. This tree uses simple key/value pairs:
typedef struct evict_mapping_key {
paddr_t paddr; // source physical address
} evict_mapping_key_t;
typedef struct evict_mapping_val {
paddr_t dst_paddr; // destination physical address
uint64_t len; // number of blocks relocated
} evict_mapping_val_t;
Conclusion
Object Maps are an essential component of APFS that serve two key roles. They provide the address translation needed to locate virtual objects on disk and enable snapshot capabilities that allow instant rollback to earlier points in time. The snapshot lifecycle (creation, deletion, and revert) leverages the OMAP’s versioned key structure and the Reaper’s multi-phase cleanup to provide efficient, crash-safe snapshot management. Object map eviction further enables live container operations like defragmentation and resize without unmounting.
Now that we can locate physical, ephemeral, and virtual objects, we have everything we need to explore the container’s remaining subsystems. In the next few posts, we will examine the Space Manager, the Reaper, and EFI Jumpstart before turning our attention to volumes and their file systems.
Find an issue or technical inaccuracy in this post? Please file an issue so that it may be corrected.