Photo of Joe T. Sylve

Joe T. Sylve, Ph.D.

Digital Forensic Researcher and Educator

Space Manager

In our earlier post on Containers, we introduced the Space Manager as the subsystem responsible for tracking which blocks are in use across all storage tiers and for allocating and freeing blocks on behalf of volumes. That post promised more detail in the future. Today we deliver on that promise by examining the Space Manager’s on-disk structures, including its hierarchical chunk tracking system, free queues, internal pool, and allocation zones.

Overview

Each APFS container has exactly one Space Manager, stored as an ephemeral object in the checkpoint data area. Its object identifier is recorded in the nx_spaceman_oid field of the NX Superblock. The Space Manager tracks block allocation using a three-tier hierarchy: the top-level spaceman_phys_t structure contains per-device metadata, which references Chunk Address Blocks (CABs) or Chunk Info Blocks (CIBs) directly, which in turn reference individual allocation bitmaps.

Chunks and Bitmaps

The Space Manager divides each storage device into fixed-size chunks. Each chunk is a contiguous range of blocks tracked by a single allocation bitmap. The number of blocks per chunk is stored in sm_blocks_per_chunk.

typedef struct chunk_info {
    uint64_t ci_xid;         // 0x00
    uint64_t ci_addr;        // 0x08
    uint32_t ci_block_count; // 0x10
    uint32_t ci_free_count;  // 0x14
    paddr_t ci_bitmap_addr;  // 0x18
} chunk_info_t;              // 0x20

Chunk Info Flags

Name Value Description
CI_PINNED_TO_MAIN 0x04000000 The chunk is within the metazone region (reserved for metadata)
CI_ALLOC_ZONE_HINT 0x08000000 The chunk is currently assigned to an allocation zone

Chunk Info Blocks and Chunk Address Blocks

Chunk info structures are grouped into Chunk Info Blocks (CIBs), physical objects that each hold an array of chunk_info_t entries.

typedef struct chunk_info_block {
    obj_phys_t cib_o;              // 0x00
    uint32_t cib_index;            // 0x20
    uint32_t cib_chunk_info_count; // 0x24
    chunk_info_t cib_chunk_info[]; // 0x28
} chunk_info_block_t;

For large containers where the number of CIBs exceeds what can be stored directly in the Space Manager, a second level of indirection is used: Chunk Address Blocks (CABs).

typedef struct cib_addr_block {
    obj_phys_t cab_o;       // 0x00
    uint32_t cab_index;     // 0x20
    uint32_t cab_cib_count; // 0x24
    paddr_t cab_cib_addr[]; // 0x28
} cib_addr_block_t;

When sm_cab_count in the device structure is zero, CIB addresses are stored directly in the Space Manager. When nonzero, the CAB indirection layer is present.

Free Queues

When blocks are freed, they are not immediately returned to the allocation bitmaps. Instead, they are placed into free queues: B-Trees that hold recently freed extents until all transactions that might reference them have been checkpointed. This ensures crash-safe deallocation.

APFS maintains three free queues:

Name Value Description
SFQ_IP 0 Internal pool free queue
SFQ_MAIN 1 Main device free queue
SFQ_TIER2 2 Tier-2 (HDD on Fusion) device free queue
typedef struct spaceman_free_queue {
    uint64_t sfq_count;           // 0x00
    oid_t sfq_tree_oid;           // 0x08
    xid_t sfq_oldest_xid;         // 0x10
    uint16_t sfq_tree_node_limit; // 0x18
    uint16_t sfq_pad16;           // 0x1A
    uint32_t sfq_pad32;           // 0x1C
    uint64_t sfq_reserved;        // 0x20
} spaceman_free_queue_t;          // 0x28

Free Queue Entries

Free queue entries use a key that sorts first by transaction identifier, then by physical address:

typedef struct spaceman_free_queue_key {
    xid_t sfqk_xid;          // 0x00
    paddr_t sfqk_paddr;      // 0x08
} spaceman_free_queue_key_t; // 0x10

The value is a uint64_t block count. Single-block extents store a zero-length value in the B-Tree to save space (the count of 1 is implied).

When inserting entries, the implementation coalesces adjacent extents that share the same transaction identifier, reducing B-Tree size and improving drain efficiency.

Internal Pool

The Internal Pool (IP) is a dedicated set of blocks used for allocating B-Tree nodes and other metadata structures. It provides a reserved area that guarantees metadata allocations can succeed even when the container is nearly full. The IP has its own allocation bitmaps, separate from the per-chunk bitmaps used for data.

Key fields in spaceman_phys_t:

When the fragmentation flag is set (bit 63 of sm_ip_block_count or bit 31 of sm_ip_bm_block_count), the pool blocks or bitmaps are not contiguous. Their physical addresses must be looked up through a Metadata Fragmented Extent List Tree rather than computed from the base address.

Allocation Zones

APFS uses allocation zones to group related allocations together on disk, reducing fragmentation and improving sequential read performance. Each device has up to 8 allocation zones (SM_DATAZONE_ALLOCZONE_COUNT), with zone IDs 1 through 4 corresponding to minimum allocation sizes in blocks.

typedef struct spaceman_allocation_zone_info_phys {
    spaceman_allocation_zone_boundaries_t saz_current_boundaries;
    spaceman_allocation_zone_boundaries_t saz_previous_boundaries[7];
    uint16_t saz_zone_id;
    uint16_t saz_previous_boundary_index;
    uint32_t saz_reserved;
} spaceman_allocation_zone_info_phys_t;

Each allocation zone boundary is a simple range:

typedef struct spaceman_allocation_zone_boundaries {
    uint64_t saz_zone_start; // 0x00
    uint64_t saz_zone_end;   // 0x08
} spaceman_allocation_zone_boundaries_t;

When an allocation zone’s current chunk becomes full, the allocator scans for a new chunk with sufficient free space, rotates the old boundaries into the circular buffer, and updates the current boundaries. The CI_ALLOC_ZONE_HINT flag on chunks tracks which chunk is currently assigned to a zone.

Metazone

The metazone is a contiguous region at the beginning of each device reserved exclusively for metadata allocation. Data allocations must not use metazone blocks. This separation ensures that metadata structures (B-Tree nodes, Space Manager bitmaps) are clustered together near the start of the device for efficient access.

The metazone size scales with device capacity:

Chunks within the metazone are marked with the CI_PINNED_TO_MAIN flag and are excluded from data allocation zones.

spaceman_phys_t

The top-level structure tying everything together:

typedef struct spaceman_phys {
    obj_phys_t sm_o;
    uint32_t sm_block_size;
    uint32_t sm_blocks_per_chunk;
    uint32_t sm_chunks_per_cib;
    uint32_t sm_cibs_per_cab;
    spaceman_device_t sm_dev[SD_COUNT];
    uint32_t sm_flags;
    uint32_t sm_ip_bm_tx_multiplier;
    uint64_t sm_ip_block_count;
    uint32_t sm_ip_bm_size_in_blocks;
    uint32_t sm_ip_bm_block_count;
    paddr_t sm_ip_bm_base;
    paddr_t sm_ip_base;
    uint64_t sm_fs_reserve_block_count;
    uint64_t sm_fs_reserve_alloc_count;
    spaceman_free_queue_t sm_fq[SFQ_COUNT];
    uint16_t sm_ip_bm_free_head;
    uint16_t sm_ip_bm_free_tail;
    uint32_t sm_ip_bm_xid_offset;
    uint32_t sm_ip_bitmap_offset;
    uint32_t sm_ip_bm_free_next_offset;
    uint32_t sm_version;
    uint32_t sm_struct_size;
    spaceman_datazone_info_phys_t sm_datazone;
    // Variable-length arrays follow...
} spaceman_phys_t;

The structure is followed by variable-length arrays: IP bitmap XID arrays, IP bitmap offset arrays, IP bitmap free-next arrays, and CIB/CAB address arrays for each device. The total on-disk size must fit within one block.

Each device is described by a spaceman_device_t:

typedef struct spaceman_device {
    uint64_t sm_block_count;  // 0x00
    uint64_t sm_chunk_count;  // 0x08
    uint32_t sm_cib_count;    // 0x10
    uint32_t sm_cab_count;    // 0x14
    uint64_t sm_free_count;   // 0x18
    uint32_t sm_addr_offset;  // 0x20
    uint32_t sm_reserved;     // 0x24
    uint64_t sm_reserved2;    // 0x28
} spaceman_device_t;          // 0x30

Forensic Considerations

The Space Manager is particularly valuable for forensic analysis:

Conclusion

The Space Manager implements a sophisticated hierarchical allocation system that balances performance, fragmentation avoidance, and crash safety. Its three-tier structure (CABs, CIBs, bitmaps) scales from tiny containers to multi-terabyte devices. Free queues ensure safe deallocation across transactions, while allocation zones and the metazone organize blocks for optimal access patterns.

Find an issue or technical inaccuracy in this post? Please file an issue so that it may be corrected.