<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://jtsylve.blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://jtsylve.blog/" rel="alternate" type="text/html" /><updated>2026-06-16T19:31:31+00:00</updated><id>https://jtsylve.blog/feed.xml</id><title type="html">Joe T. Sylve, Ph.D.</title><subtitle>Digital Forensic Researcher and Educator</subtitle><entry><title type="html">Speculative Telemetry</title><link href="https://jtsylve.blog/post/2026/06/12/APFS-Speculative-Telemetry" rel="alternate" type="text/html" title="Speculative Telemetry" /><published>2026-06-12T00:00:00+00:00</published><updated>2026-06-15T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2026/06/12/APFS%20Speculative%20Telemetry</id><content type="html" xml:base="https://jtsylve.blog/post/2026/06/12/APFS-Speculative-Telemetry"><![CDATA[<p>Speculative telemetry is an APFS feature that tracks the lifecycle of speculatively downloaded files: content fetched to local storage before the user explicitly requests it, such as files prefetched by iCloud or the App Store. This post covers the on-disk structures and state machine that enable this tracking.</p>

<h2 id="overview">Overview</h2>

<p>On eligible volumes (Data, Enterprise, or User role), APFS records residency state transitions for speculatively downloaded files: when they are materialized (downloaded), accessed, evicted (data removed but inode retained), or purged (fully deleted). This information helps the system optimize its prefetch decisions by understanding which speculative downloads were actually useful.</p>

<p>Tracking is active only when bit 0 of the <code class="language-plaintext highlighter-rouge">spec_telem_enablement</code> boot-arg (or sysctl) is set, and only on volumes that are not snapshot-mounted.</p>

<h2 id="inode-flags">Inode Flags</h2>

<p>Two flags in <code class="language-plaintext highlighter-rouge">j_inode_val_t.internal_flags</code> control participation:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define INODE_MAINTAIN_SPECULATIVE_TELEMETRY 0x20000000  // bit 29
#define INODE_SPECULATIVE_TELEMETRY_ACTIVE   0x40000000  // bit 30
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">INODE_MAINTAIN_SPECULATIVE_TELEMETRY</code> is inherited from parent directories (part of the inherited flags mask). It marks an inode or directory as eligible for telemetry tracking. <code class="language-plaintext highlighter-rouge">INODE_SPECULATIVE_TELEMETRY_ACTIVE</code> is set when the inode qualifies for active tracking and causes creation or update of the telemetry extended attribute.</p>

<h2 id="extended-attribute">Extended Attribute</h2>

<p>Telemetry data is stored in a file-system-owned, embedded extended attribute:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define SPECULATIVE_TELEMETRY_EA_NAME "com.apple.system.fs.speculative_telemetry"
</span>
<span class="k">typedef</span> <span class="k">struct</span> <span class="nc">spec_telemetry_xattr</span> <span class="p">{</span>
    <span class="kt">uint8_t</span> <span class="n">version</span><span class="p">;</span>      <span class="c1">// 0x00 (must be 0)</span>
    <span class="kt">uint8_t</span> <span class="n">use_state</span><span class="p">;</span>    <span class="c1">// 0x01</span>
    <span class="kt">uint16_t</span> <span class="n">flags</span><span class="p">;</span>       <span class="c1">// 0x02</span>
    <span class="kt">uint64_t</span> <span class="n">timestamp</span><span class="p">;</span>   <span class="c1">// 0x04</span>
<span class="p">}</span> <span class="n">spec_telemetry_xattr_t</span><span class="p">;</span> <span class="c1">// 0x0C (12 bytes)</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">version</code>: Must be 0 (future versions are rejected)</li>
  <li><code class="language-plaintext highlighter-rouge">use_state</code>: The current residency/usage state</li>
  <li><code class="language-plaintext highlighter-rouge">flags</code>: Bit 0 = dirty (state changed but fsevent not yet sent), bits 2-5 = residency reason</li>
  <li><code class="language-plaintext highlighter-rouge">timestamp</code>: APFS timestamp (nanoseconds since epoch) of the last state transition</li>
</ul>

<p>This attribute is always 12 bytes and cannot be set from userland.</p>

<h2 id="use-states">Use States</h2>

<table style="margin-left: 0">
  <thead>
    <tr>
      <th>Value</th>
      <th>Name</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>None</td>
      <td>No telemetry state recorded</td>
    </tr>
    <tr>
      <td>1</td>
      <td>Materialized</td>
      <td>File data was downloaded to local storage</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Evicted</td>
      <td>File data was removed from local storage (inode remains)</td>
    </tr>
    <tr>
      <td>3</td>
      <td>Purged</td>
      <td>File was fully purged (deleted)</td>
    </tr>
    <tr>
      <td>4</td>
      <td>Accessed</td>
      <td>File data was accessed by a user or process</td>
    </tr>
    <tr>
      <td>5</td>
      <td>Downloaded</td>
      <td>File was explicitly downloaded (not speculative)</td>
    </tr>
    <tr>
      <td>6</td>
      <td>Reserved</td>
      <td>No-op (no state change)</td>
    </tr>
  </tbody>
</table>

<h2 id="residency-reasons">Residency Reasons</h2>

<p>The <code class="language-plaintext highlighter-rouge">flags</code> field (bits 2-5) encodes why the file was speculatively downloaded. This allows the system to distinguish between different prefetch strategies and measure their effectiveness. APFS validates the range (0-6) but does not interpret the value; the semantic meanings are defined by the FileProvider framework:</p>

<table style="margin-left: 0">
  <thead>
    <tr>
      <th>Value</th>
      <th>Name</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td><code class="language-plaintext highlighter-rouge">recents</code></td>
      <td>Appeared in the user’s recent-documents set</td>
    </tr>
    <tr>
      <td>2</td>
      <td><code class="language-plaintext highlighter-rouge">speculativeUpdates</code></td>
      <td>Background prefetch by the speculative-downloads subsystem</td>
    </tr>
    <tr>
      <td>3</td>
      <td><code class="language-plaintext highlighter-rouge">createdLocallyOrUserRequestedOnOlderBuild</code></td>
      <td>Legacy ambiguous reason from older builds</td>
    </tr>
    <tr>
      <td>4</td>
      <td><code class="language-plaintext highlighter-rouge">providerRequested</code></td>
      <td>The cloud provider extension requested materialization</td>
    </tr>
    <tr>
      <td>5</td>
      <td><code class="language-plaintext highlighter-rouge">createdLocally</code></td>
      <td>Created on this device (not downloaded)</td>
    </tr>
    <tr>
      <td>6</td>
      <td><code class="language-plaintext highlighter-rouge">userRequested</code></td>
      <td>The user explicitly triggered the download</td>
    </tr>
  </tbody>
</table>

<h2 id="lifecycle">Lifecycle</h2>

<ol>
  <li>
    <p><strong>Eligibility</strong>: An inode qualifies for telemetry if it is a regular file or directory, <code class="language-plaintext highlighter-rouge">INODE_MAINTAIN_SPECULATIVE_TELEMETRY</code> is set, the volume has an eligible role, and <code class="language-plaintext highlighter-rouge">spec_telem_enablement</code> bit 0 is set.</p>
  </li>
  <li>
    <p><strong>Creation</strong>: When a qualifying inode is created in a tracked directory, <code class="language-plaintext highlighter-rouge">INODE_SPECULATIVE_TELEMETRY_ACTIVE</code> is set.</p>
  </li>
  <li>
    <p><strong>Materialization</strong>: When speculative content is downloaded, the extended attribute is created with <code class="language-plaintext highlighter-rouge">use_state = 1</code> (Materialized) and the current timestamp.</p>
  </li>
  <li>
    <p><strong>Access</strong>: When the file is read by a user or process, the state transitions to <code class="language-plaintext highlighter-rouge">4</code> (Accessed). This is the key metric: speculative downloads that are accessed were useful.</p>
  </li>
  <li>
    <p><strong>Eviction</strong>: When the system reclaims space by removing the file’s data (while keeping the inode), the state transitions to <code class="language-plaintext highlighter-rouge">2</code> (Evicted).</p>
  </li>
  <li>
    <p><strong>Purge</strong>: When the file is fully deleted, the state transitions to <code class="language-plaintext highlighter-rouge">3</code> (Purged).</p>
  </li>
  <li>
    <p><strong>Event Reporting</strong>: When the telemetry state has changed but the corresponding event has not yet been reported, the dirty bit (<code class="language-plaintext highlighter-rouge">SPEC_TELEM_FLAG_DIRTY</code>, bit 0 of <code class="language-plaintext highlighter-rouge">flags</code>) is set. It is cleared before applying the next state change.</p>
  </li>
</ol>

<h2 id="directory-integration">Directory Integration</h2>

<p>For directories with <code class="language-plaintext highlighter-rouge">INODE_MAINTAIN_DIR_STATS</code>, telemetry participation is tracked at the directory level through the <code class="language-plaintext highlighter-rouge">telemetry_count</code> field of the expanded directory-statistics record (<code class="language-plaintext highlighter-rouge">j_dir_stats_expanded_val_t</code>), updated during reconciliation. This enables aggregate reporting of speculative download effectiveness per directory hierarchy. (Note that <code class="language-plaintext highlighter-rouge">0x100</code> in the directory-statistics flag set is <code class="language-plaintext highlighter-rouge">DIR_STATS_INITIALIZED</code>, which is unrelated to telemetry.)</p>

<h2 id="inode-extended-fields">Inode Extended Fields</h2>

<p>Two legacy extended field types support telemetry:</p>

<table style="margin-left: 0">
  <thead>
    <tr>
      <th>Type</th>
      <th>Value</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>INO_EXT_TYPE_SPEC_TELEMETRY_STATE</td>
      <td>20</td>
      <td>Legacy 2-byte field (read and immediately removed; superseded by the extended attribute)</td>
    </tr>
    <tr>
      <td>INO_EXT_TYPE_SPEC_TELEMETRY_TRIGGER</td>
      <td>22</td>
      <td>8-byte trigger information recorded when a purgeable file is removed after being accessed</td>
    </tr>
  </tbody>
</table>

<h2 id="forensic-considerations">Forensic Considerations</h2>

<ul>
  <li>The <code class="language-plaintext highlighter-rouge">com.apple.system.fs.speculative_telemetry</code> extended attribute reveals which files were speculatively downloaded and whether they were ever accessed.</li>
  <li>The timestamp field provides precise timing of state transitions.</li>
  <li>The residency reason identifies the prefetch strategy that triggered the download.</li>
  <li>Files in the “Materialized” state (never accessed) represent wasted bandwidth and storage.</li>
  <li>The <code class="language-plaintext highlighter-rouge">INODE_MAINTAIN_SPECULATIVE_TELEMETRY</code> flag on directories identifies which directory hierarchies are managed by cloud sync services.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Speculative telemetry gives APFS visibility into the effectiveness of speculative downloads. By tracking the complete lifecycle from materialization through access or eviction, the system can make informed decisions about which content to prefetch. For forensic analysis, these records reveal cloud sync activity, file access patterns, and storage management decisions.</p>]]></content><author><name></name></author><category term="file-systems" /><category term="apfs" /><category term="apfs" /><category term="telemetry" /><category term="cloud" /><summary type="html"><![CDATA[Speculative telemetry is an APFS feature that tracks the lifecycle of speculatively downloaded files: content fetched to local storage before the user explicitly requests it, such as files prefetched by iCloud or the App Store. This post covers the on-disk structures and state machine that enable this tracking.]]></summary></entry><entry><title type="html">Volume Grafting</title><link href="https://jtsylve.blog/post/2026/06/11/APFS-Grafting" rel="alternate" type="text/html" title="Volume Grafting" /><published>2026-06-11T00:00:00+00:00</published><updated>2026-06-15T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2026/06/11/APFS%20Grafting</id><content type="html" xml:base="https://jtsylve.blog/post/2026/06/11/APFS-Grafting"><![CDATA[<p>Volume grafting is a mechanism introduced in macOS 13 that mounts a disk image’s APFS contents as a subdirectory of an existing volume. This is the technology behind <em>Cryptexes</em>, the cryptographically sealed, graftable disk images used for Rapid Security Responses and system extensions. This post covers the graft lifecycle, constraints, and on-disk metadata.</p>

<h2 id="overview">Overview</h2>

<p>A graft takes an APFS-formatted disk image file that resides on a host volume and mounts its file system tree under a designated directory on that same volume. To applications, the grafted content appears as ordinary files and directories within the host volume’s namespace. Up to 255 grafts can be active on a single volume simultaneously.</p>

<p>The kernel builds a <em>blockmap LUT</em> (lookup table) that maps logical block addresses within the graft image to physical blocks on the host volume’s storage. This allows the grafted file system to be read using the same block I/O path as the host volume.</p>

<h2 id="graft-constraints">Graft Constraints</h2>

<ul>
  <li>Maximum 255 grafts per volume</li>
  <li>The graft file must be a regular file with nonzero size</li>
  <li>The graft file must not have hard links (<code class="language-plaintext highlighter-rouge">nlink</code> must be less than 2)</li>
  <li>The graft file must not be compressed</li>
  <li>Grafts cannot be nested: the graft file must not reside inside another graft</li>
  <li>The graft directory must be an existing, non-deleted directory that is not already a graft point</li>
  <li>The host volume must not itself be a graft</li>
  <li>On encrypted volumes, the file must use protection class C or D; cloned files and empty files are rejected</li>
  <li>Volumes undergoing crypto transformation cannot graft</li>
</ul>

<h2 id="graft-extended-attributes">Graft Extended Attributes</h2>

<p>Three extended attributes on the graft file’s inode record the J-object ID reservation:</p>

<table style="margin-left: 0">
  <thead>
    <tr>
      <th>Name</th>
      <th>Size</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">com.apple.fs.graft-vol-uuid</code></td>
      <td>16 bytes</td>
      <td>UUID of the host volume at graft time</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">com.apple.fs.graft-jobj-id-base</code></td>
      <td>8 bytes</td>
      <td>Base (start) of the reserved J-object ID range</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">com.apple.fs.graft-jobj-id-len</code></td>
      <td>8 bytes</td>
      <td>Length (count) of the reserved J-object ID range</td>
    </tr>
  </tbody>
</table>

<p>These attributes are file-system-owned (<code class="language-plaintext highlighter-rouge">XATTR_FILE_SYSTEM_OWNED</code>) and persist after ungraft, allowing subsequent grafts of the same file to reclaim the same ID range without conflicts.</p>

<h2 id="graft-lifecycle">Graft Lifecycle</h2>

<h3 id="phase-1-validation">Phase 1: Validation</h3>

<p>The kernel verifies that all constraints are met: the file exists, is not compressed, has no hard links, the directory is valid, and the volume has capacity for another graft.</p>

<h3 id="phase-2-blockmap-lut-construction">Phase 2: Blockmap LUT Construction</h3>

<p>The kernel iterates all data extents of the graft file and builds an in-memory B-Tree (subtype <code class="language-plaintext highlighter-rouge">OBJECT_TYPE_GRAFT_BLOCKMAP_LUT_TREE</code>) that maps logical blocks within the image to physical blocks on the host volume. If the file is a clone of an already-grafted file, the existing blockmap is shared.</p>

<h3 id="phase-3-encryption">Phase 3: Encryption</h3>

<p>On encrypted volumes, the graft file’s encryption key is unwrapped and retained for later I/O translation.</p>

<h3 id="phase-4-container-and-volume-mount">Phase 4: Container and Volume Mount</h3>

<p>The APFS container embedded in the graft image is mounted using the blockmap LUT for I/O translation. The first volume within the grafted container is then mounted.</p>

<h3 id="phase-5-metadata-lut-enhancement">Phase 5: Metadata LUT Enhancement</h3>

<p>The blockmap is augmented with metadata block mappings (container superblock, checkpoint areas, space manager, object maps, B-Tree nodes). Metadata blocks are distinguished from data blocks by having bit 31 set in the LUT key.</p>

<h3 id="phase-6-image4-authentication">Phase 6: Image4 Authentication</h3>

<p>For sealed graft images (Cryptexes), the volume’s root hash is verified against an Image4 payload and manifest. Authentication volume types include:</p>

<table style="margin-left: 0">
  <thead>
    <tr>
      <th>Type</th>
      <th>Name</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>4</td>
      <td>RSR Graft</td>
      <td>Rapid Security Response (authentication required, except skipped on internal builds)</td>
    </tr>
    <tr>
      <td>5</td>
      <td>Strict Graft</td>
      <td>Authentication failure is fatal: the mount is refused unconditionally and never degrades to an unauthenticated mount</td>
    </tr>
  </tbody>
</table>

<h3 id="phase-7-j-object-id-range-reservation">Phase 7: J-Object ID Range Reservation</h3>

<p>A range of object identifiers is reserved from the host volume’s ID space for the grafted content. This ensures grafted inodes do not collide with host volume inodes. The reservation is persisted in the graft extended attributes.</p>

<h3 id="phase-8-state-registration">Phase 8: State Registration</h3>

<p>The graft state is registered, a synthetic device ID is generated, and grafted file-system vnodes are loaded. An IOKit <code class="language-plaintext highlighter-rouge">AppleAPFSGraft</code> service node is published.</p>

<h2 id="ungraft">Ungraft</h2>

<p>The ungraft operation reverses the graft:</p>

<ol>
  <li>Remove graft state and decrement the volume’s graft count</li>
  <li>Wait for all concurrent readers to drain</li>
  <li>Revoke vnodes belonging to the graft</li>
  <li>Detach the IOKit service node</li>
  <li>Unmount the grafted volume and container</li>
  <li>Clear the graft inode flags (<code class="language-plaintext highlighter-rouge">INODE_IS_GRAFT_DIR</code> on the directory, <code class="language-plaintext highlighter-rouge">INODE_IS_GRAFT_FILE</code> on the file)</li>
  <li>Release crypto state if present</li>
</ol>

<p>The ungraft ioctl supports flags for ungrafting all grafts on a volume (bit 0) and forcing ungraft even when vnodes are in use (bit 1).</p>

<h2 id="forensic-considerations">Forensic Considerations</h2>

<ul>
  <li>The principal on-disk artifact of an active graft is a set of inode flags stored in <code class="language-plaintext highlighter-rouge">j_inode_val_t.internal_flags</code> (not <code class="language-plaintext highlighter-rouge">bsd_flags</code>): <code class="language-plaintext highlighter-rouge">INODE_IS_GRAFT_FILE</code> (bit 49, <code class="language-plaintext highlighter-rouge">0x2000000000000</code>) on the host file, <code class="language-plaintext highlighter-rouge">INODE_IS_GRAFT_DIR</code> (bit 47, <code class="language-plaintext highlighter-rouge">0x800000000000</code>) on the graft directory, and <code class="language-plaintext highlighter-rouge">INODE_INSIDE_GRAFT</code> (bit 46, <code class="language-plaintext highlighter-rouge">0x400000000000</code>) on every inode belonging to the grafted tree. Bits 46 and 47 together identify any inode participating in a graft.</li>
  <li>The graft extended attributes (<code class="language-plaintext highlighter-rouge">com.apple.fs.graft-*</code>) on a file indicate it was used as a graft image, even if no graft is currently active.</li>
  <li>The <code class="language-plaintext highlighter-rouge">graft-vol-uuid</code> must match the current volume UUID, providing a way to verify the graft’s provenance.</li>
  <li>The reserved J-object ID range reveals which inode numbers belong to grafted content.</li>
  <li>On sealed system volumes, grafts (Cryptexes) provide the mechanism for Rapid Security Responses: security patches can be applied without modifying the sealed system volume itself.</li>
  <li>The blockmap LUT is memory-only; it is not persisted on disk. Forensic analysis of grafted content requires parsing the graft image file’s extents to reconstruct the logical-to-physical mapping.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Volume grafting extends APFS’s capabilities by allowing sealed disk images to be mounted as subdirectories. Combined with Image4 authentication, this provides a secure mechanism for distributing system extensions and security updates (Cryptexes) without breaking the sealed system volume’s integrity guarantees.</p>]]></content><author><name></name></author><category term="file-systems" /><category term="apfs" /><category term="apfs" /><category term="grafting" /><category term="cryptex" /><summary type="html"><![CDATA[Volume grafting is a mechanism introduced in macOS 13 that mounts a disk image’s APFS contents as a subdirectory of an existing volume. This is the technology behind Cryptexes, the cryptographically sealed, graftable disk images used for Rapid Security Responses and system extensions. This post covers the graft lifecycle, constraints, and on-disk metadata.]]></summary></entry><entry><title type="html">Encryption Rolling</title><link href="https://jtsylve.blog/post/2026/06/10/APFS-Encryption-Rolling" rel="alternate" type="text/html" title="Encryption Rolling" /><published>2026-06-10T00:00:00+00:00</published><updated>2026-06-10T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2026/06/10/APFS%20Encryption%20Rolling</id><content type="html" xml:base="https://jtsylve.blog/post/2026/06/10/APFS-Encryption-Rolling"><![CDATA[<p>In our posts on <a href="/post/2022/12/21/APFS-Keybags">Keybags</a>, <a href="/post/2022/12/22/APFS-Wrapped-Keys">Wrapped Keys</a>, and <a href="/post/2022/12/26/APFS-Decryption">Decryption</a>, we covered the static encryption architecture of APFS: how keys are stored, unwrapped, and used to decrypt data. This post covers <em>encryption rolling</em>, the background process that encrypts, decrypts, or re-keys an entire volume’s data while the system continues operating.</p>

<h2 id="overview">Overview</h2>

<p>Encryption rolling is triggered when a volume transitions between encryption states: from unencrypted to encrypted, from encrypted to unencrypted, or from one key to another. Because a volume may contain terabytes of data, this operation cannot complete in a single transaction. Instead, APFS maintains an <code class="language-plaintext highlighter-rouge">er_state_phys_t</code> object that tracks progress across transactions, allowing the operation to resume after crashes or reboots.</p>

<p>The encryption rolling state object is referenced by the <code class="language-plaintext highlighter-rouge">apfs_er_state_oid</code> field of the <a href="/post/2022/12/13/APFS-Volume-Superblock">Volume Superblock</a>.</p>

<h2 id="phases">Phases</h2>

<p>Encryption rolling proceeds through three phases, tracked in the <code class="language-plaintext highlighter-rouge">ersb_flags</code> field. The <code class="language-plaintext highlighter-rouge">er_phase_t</code> values below are not stored directly: they are shifted into bits 12-13 of <code class="language-plaintext highlighter-rouge">ersb_flags</code> (extract via <code class="language-plaintext highlighter-rouge">ERSB_FLAG_ER_PHASE_MASK</code> <code class="language-plaintext highlighter-rouge">0x3000</code> and <code class="language-plaintext highlighter-rouge">ERSB_FLAG_ER_PHASE_SHIFT</code> <code class="language-plaintext highlighter-rouge">12</code>), so <code class="language-plaintext highlighter-rouge">ER_PHASE_DATA_ROLL</code> (2) appears on disk as <code class="language-plaintext highlighter-rouge">0x2000</code>.</p>

<table style="margin-left: 0">
  <thead>
    <tr>
      <th>Phase</th>
      <th>Value</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ER_PHASE_OMAP_ROLL</td>
      <td>1</td>
      <td>Rolling the volume’s Object Map nodes</td>
    </tr>
    <tr>
      <td>ER_PHASE_DATA_ROLL</td>
      <td>2</td>
      <td>Rolling file data extents</td>
    </tr>
    <tr>
      <td>ER_PHASE_SNAP_ROLL</td>
      <td>3</td>
      <td>Rolling snapshot data</td>
    </tr>
  </tbody>
</table>

<p>Each phase processes its objects in windows, encrypting or decrypting a chunk of data at a time (typically 1 MiB per window).</p>

<h2 id="er_state_phys_t">er_state_phys_t</h2>

<p>The on-disk encryption rolling state (version 2, 128 bytes total).</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define ER_MAGIC 0x464C4142 // 'FLAB'
</span>
<span class="k">typedef</span> <span class="k">struct</span> <span class="nc">er_state_phys_header</span> <span class="p">{</span>
    <span class="n">obj_phys_t</span> <span class="n">ersb_o</span><span class="p">;</span>     <span class="c1">// 0x00</span>
    <span class="kt">uint32_t</span> <span class="n">ersb_magic</span><span class="p">;</span>   <span class="c1">// 0x20</span>
    <span class="kt">uint32_t</span> <span class="n">ersb_version</span><span class="p">;</span> <span class="c1">// 0x24</span>
<span class="p">}</span> <span class="n">er_state_phys_header_t</span><span class="p">;</span>  <span class="c1">// 0x28</span>

<span class="k">typedef</span> <span class="k">struct</span> <span class="nc">er_state_phys</span> <span class="p">{</span>
    <span class="n">er_state_phys_header_t</span> <span class="n">ersb_header</span><span class="p">;</span>        <span class="c1">// 0x00</span>
    <span class="kt">uint64_t</span> <span class="n">ersb_flags</span><span class="p">;</span>                       <span class="c1">// 0x28</span>
    <span class="kt">uint64_t</span> <span class="n">ersb_snap_xid</span><span class="p">;</span>                    <span class="c1">// 0x30</span>
    <span class="kt">uint64_t</span> <span class="n">ersb_current_fext_obj_id</span><span class="p">;</span>         <span class="c1">// 0x38</span>
    <span class="kt">uint64_t</span> <span class="n">ersb_file_offset</span><span class="p">;</span>                 <span class="c1">// 0x40</span>
    <span class="kt">uint64_t</span> <span class="n">ersb_progress</span><span class="p">;</span>                    <span class="c1">// 0x48</span>
    <span class="kt">uint64_t</span> <span class="n">ersb_total_blk_to_encrypt</span><span class="p">;</span>        <span class="c1">// 0x50</span>
    <span class="n">oid_t</span> <span class="n">ersb_blockmap_oid</span><span class="p">;</span>                   <span class="c1">// 0x58</span>
    <span class="kt">uint64_t</span> <span class="n">ersb_tidemark_obj_id</span><span class="p">;</span>             <span class="c1">// 0x60</span>
    <span class="kt">uint64_t</span> <span class="n">ersb_recovery_extents_count</span><span class="p">;</span>      <span class="c1">// 0x68</span>
    <span class="n">oid_t</span> <span class="n">ersb_recovery_list_oid</span><span class="p">;</span>              <span class="c1">// 0x70</span>
    <span class="kt">uint64_t</span> <span class="n">ersb_recovery_length</span><span class="p">;</span>             <span class="c1">// 0x78</span>
<span class="p">}</span> <span class="n">er_state_phys_t</span><span class="p">;</span>                             <span class="c1">// 0x80</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">ersb_header</code>: Object header with magic (<code class="language-plaintext highlighter-rouge">'FLAB'</code>) and version (1 or 2)</li>
  <li><code class="language-plaintext highlighter-rouge">ersb_flags</code>: Operation type, phase, checksum block size, and status flags</li>
  <li><code class="language-plaintext highlighter-rouge">ersb_snap_xid</code>: Transaction identifier of the snapshot used as the rolling baseline</li>
  <li><code class="language-plaintext highlighter-rouge">ersb_current_fext_obj_id</code>: Object identifier of the file extent currently being processed</li>
  <li><code class="language-plaintext highlighter-rouge">ersb_file_offset</code>: Byte offset within the current file</li>
  <li><code class="language-plaintext highlighter-rouge">ersb_progress</code>: Number of blocks processed so far</li>
  <li><code class="language-plaintext highlighter-rouge">ersb_total_blk_to_encrypt</code>: Total blocks that need processing</li>
  <li><code class="language-plaintext highlighter-rouge">ersb_blockmap_oid</code>: Object identifier of the rolling block map</li>
  <li><code class="language-plaintext highlighter-rouge">ersb_tidemark_obj_id</code>: Tracks the boundary between processed and unprocessed data</li>
  <li><code class="language-plaintext highlighter-rouge">ersb_recovery_extents_count</code>: Number of recovery extents for crash recovery</li>
  <li><code class="language-plaintext highlighter-rouge">ersb_recovery_list_oid</code>: Object identifier of the recovery extent list</li>
  <li><code class="language-plaintext highlighter-rouge">ersb_recovery_length</code>: Total length of recovery data in blocks</li>
</ul>

<h2 id="encryption-rolling-flags">Encryption Rolling Flags</h2>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define ERSB_FLAG_ENCRYPTING       0x00000001
#define ERSB_FLAG_DECRYPTING       0x00000002
#define ERSB_FLAG_KEYROLLING       0x00000004
#define ERSB_FLAG_PAUSED           0x00000008
#define ERSB_FLAG_FAILED           0x00000010
#define ERSB_FLAG_CID_IS_TWEAK     0x00000020
#define ERSB_FLAG_CM_BLOCK_SIZE_MASK  0x00000F00
#define ERSB_FLAG_CM_BLOCK_SIZE_SHIFT 8
#define ERSB_FLAG_ER_PHASE_MASK    0x00003000
#define ERSB_FLAG_ER_PHASE_SHIFT   12
#define ERSB_FLAG_FROM_ONEKEY      0x00004000
</span></code></pre></div></div>

<p>The operation type is one of <code class="language-plaintext highlighter-rouge">ENCRYPTING</code>, <code class="language-plaintext highlighter-rouge">DECRYPTING</code>, or <code class="language-plaintext highlighter-rouge">KEYROLLING</code> (key rolling is not supported in the current implementation). The current phase is extracted from bits 12-13. The checksum block size (bits 8-11) encodes the hardware encryption block size used for integrity checks.</p>

<h2 id="rolling-window-algorithm">Rolling Window Algorithm</h2>

<p>The rolling process operates on a window of file extents at a time:</p>

<h3 id="pre-roll-phase">Pre-Roll Phase</h3>
<ol>
  <li>Enter a transaction.</li>
  <li>Lock file extents for the current window.</li>
  <li>For each extent, compute AES-XTS tweaks and read the unrolled data.</li>
  <li>Compute SHA-256 checksums for each checksum-block-sized chunk and store truncated hashes in a recovery buffer.</li>
  <li>Write recovery data to disk so the operation can resume after a crash.</li>
  <li>Commit the transaction.</li>
</ol>

<h3 id="data-roll-phase">Data Roll Phase</h3>
<ol>
  <li>Encrypt (or decrypt) each extent’s data in-place using XTS-AES.</li>
  <li>Write the rolled data back to its physical location.</li>
  <li>If a write fails, retry up to 30 times with a 1-second sleep between attempts. After exhausting retries, mark the operation as failed.</li>
</ol>

<h3 id="post-roll-phase">Post-Roll Phase</h3>
<ol>
  <li>Enter a new transaction.</li>
  <li>Update the block map to record the new encryption state.</li>
  <li>For decryption, update file extent records to remove the crypto association.</li>
  <li>Delete recovery data.</li>
  <li>Update progress counters.</li>
  <li>Commit.</li>
</ol>

<h2 id="recovery-blocks">Recovery Blocks</h2>

<p>Recovery blocks store data needed for crash recovery during the data roll phase. If the system crashes after writing encrypted data but before committing the post-roll transaction, the recovery data allows the operation to be verified and resumed.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">er_recovery_block_phys</span> <span class="p">{</span>
    <span class="n">obj_phys_t</span> <span class="n">erb_o</span><span class="p">;</span>       <span class="c1">// 0x00</span>
    <span class="kt">uint64_t</span> <span class="n">erb_offset</span><span class="p">;</span>    <span class="c1">// 0x20</span>
    <span class="n">oid_t</span> <span class="n">erb_next_oid</span><span class="p">;</span>     <span class="c1">// 0x28</span>
    <span class="kt">uint8_t</span> <span class="n">erb_data</span><span class="p">[];</span>     <span class="c1">// 0x30</span>
<span class="p">}</span> <span class="n">er_recovery_block_phys_t</span><span class="p">;</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">erb_o</code>: The object’s header</li>
  <li><code class="language-plaintext highlighter-rouge">erb_offset</code>: Byte offset into the recovery data stream</li>
  <li><code class="language-plaintext highlighter-rouge">erb_next_oid</code>: Object identifier of the next recovery block, or zero</li>
  <li><code class="language-plaintext highlighter-rouge">erb_data</code>: Recovery data payload (checksums of the pre-roll data)</li>
</ul>

<h2 id="general-purpose-bitmaps">General-Purpose Bitmaps</h2>

<p>A <em>general-purpose bitmap</em> tracks per-block rolling state, indicating which blocks have been processed and which remain.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">gbitmap_phys</span> <span class="p">{</span>
    <span class="n">obj_phys_t</span> <span class="n">bm_o</span><span class="p">;</span>       <span class="c1">// 0x00</span>
    <span class="n">oid_t</span> <span class="n">bm_tree_oid</span><span class="p">;</span>     <span class="c1">// 0x20</span>
    <span class="kt">uint64_t</span> <span class="n">bm_bit_count</span><span class="p">;</span> <span class="c1">// 0x28</span>
    <span class="kt">uint64_t</span> <span class="n">bm_flags</span><span class="p">;</span>     <span class="c1">// 0x30</span>
<span class="p">}</span> <span class="n">gbitmap_phys_t</span><span class="p">;</span>          <span class="c1">// 0x38</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">bm_o</code>: The object’s header</li>
  <li><code class="language-plaintext highlighter-rouge">bm_tree_oid</code>: Object identifier of the B-Tree storing bitmap blocks</li>
  <li><code class="language-plaintext highlighter-rouge">bm_bit_count</code>: Total number of bits in the bitmap</li>
  <li><code class="language-plaintext highlighter-rouge">bm_flags</code>: Reserved (zero)</li>
</ul>

<p>The bitmap blocks themselves are stored as <code class="language-plaintext highlighter-rouge">gbitmap_block_phys_t</code> objects containing the raw bitmap data. Each bit represents one block in the volume; a set bit indicates the block has been rolled.</p>

<h2 id="forensic-considerations">Forensic Considerations</h2>

<p>Encryption rolling state reveals important information:</p>

<ul>
  <li>A non-zero <code class="language-plaintext highlighter-rouge">apfs_er_state_oid</code> in the Volume Superblock indicates an encryption transition was in progress (or interrupted).</li>
  <li>The <code class="language-plaintext highlighter-rouge">ersb_progress</code> and <code class="language-plaintext highlighter-rouge">ersb_total_blk_to_encrypt</code> fields reveal how far the operation had progressed.</li>
  <li>The <code class="language-plaintext highlighter-rouge">ERSB_FLAG_FAILED</code> or <code class="language-plaintext highlighter-rouge">ERSB_FLAG_PAUSED</code> flags indicate an interrupted or failed transition.</li>
  <li>During a partial roll, some blocks are encrypted and others are not. The general-purpose bitmap identifies which blocks have been processed, enabling correct decryption of mixed-state volumes.</li>
  <li>The <code class="language-plaintext highlighter-rouge">ERSB_FLAG_FROM_ONEKEY</code> flag indicates the volume was previously encrypted with a per-volume key, which affects tweak computation for unrolled blocks.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Encryption rolling provides crash-safe, incremental encryption state transitions for APFS volumes. Its multi-phase design (OMAP, data, snapshots) ensures all volume data is processed, while recovery blocks and general-purpose bitmaps enable correct resumption after interruptions. Understanding this mechanism is essential for forensic analysis of volumes in transitional encryption states.</p>]]></content><author><name></name></author><category term="file-systems" /><category term="apfs" /><category term="apfs" /><category term="encryption" /><category term="rolling" /><summary type="html"><![CDATA[In our posts on Keybags, Wrapped Keys, and Decryption, we covered the static encryption architecture of APFS: how keys are stored, unwrapped, and used to decrypt data. This post covers encryption rolling, the background process that encrypts, decrypts, or re-keys an entire volume’s data while the system continues operating.]]></summary></entry><entry><title type="html">Clonegroups</title><link href="https://jtsylve.blog/post/2026/06/09/APFS-Clonegroups" rel="alternate" type="text/html" title="Clonegroups" /><published>2026-06-09T00:00:00+00:00</published><updated>2026-06-09T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2026/06/09/APFS%20Clonegroups</id><content type="html" xml:base="https://jtsylve.blog/post/2026/06/09/APFS-Clonegroups"><![CDATA[<p>In our <a href="/post/2022/12/19/APFS-Data-Streams">post on Data Streams</a>, we discussed how APFS implements file cloning through shared extents and reference counting. While <code class="language-plaintext highlighter-rouge">j_phys_ext_val_t</code> reference counts and <code class="language-plaintext highlighter-rouge">j_dstream_id_val_t</code> track sharing at the extent level, APFS also maintains a higher-level grouping mechanism called <em>clonegroups</em> that tracks which inodes share physical data. This post covers the clonegroup tree and its role in managing cloned files.</p>

<h2 id="overview">Overview</h2>

<p>The <em>clonegroup tree</em> tracks groups of files that share physical data extents through cloning (e.g., <code class="language-plaintext highlighter-rouge">cp --clone</code> or the <code class="language-plaintext highlighter-rouge">clonefile</code> syscall). It is a <a href="/post/2022/12/08/APFS-BTrees">B-Tree</a> with subtype <code class="language-plaintext highlighter-rouge">OBJECT_TYPE_CLONEGROUP_TREE</code>, referenced by the <code class="language-plaintext highlighter-rouge">apfs_clonegroup_tree_oid</code> field in the <a href="/post/2022/12/13/APFS-Volume-Superblock">Volume Superblock</a>.</p>

<p>Within each clone group, exactly one inode is designated the <em>full clone</em>: it owns the physical data extents shared by the group. All other members are <em>partial clones</em> that reference the full clone’s extents via copy-on-write. When an inode has a <code class="language-plaintext highlighter-rouge">INO_EXT_TYPE_CLONEGROUP_ID</code> (type 21) extended field set, it belongs to the clone group identified by that field’s value.</p>

<h2 id="record-types">Record Types</h2>

<p>The clonegroup tree contains two types of records, distinguished by a <code class="language-plaintext highlighter-rouge">record_type</code> field in the key:</p>

<table style="margin-left: 0">
  <thead>
    <tr>
      <th>Type</th>
      <th>Name</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Mapping</td>
      <td>Maps an inode to a clone group. One record per member inode.</td>
    </tr>
    <tr>
      <td>2</td>
      <td>Cookie</td>
      <td>Inserted when only one member remains, signaling the group can be cleaned up.</td>
    </tr>
  </tbody>
</table>

<h2 id="on-disk-structures">On-Disk Structures</h2>

<h3 id="mapping-records-record_type--1">Mapping Records (record_type = 1)</h3>

<p>Mapping records track which inodes belong to a clone group.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">clonegroup_mapping_key</span> <span class="p">{</span>
    <span class="kt">uint64_t</span> <span class="n">group_id</span><span class="p">;</span>     <span class="c1">// 0x00</span>
    <span class="kt">uint8_t</span> <span class="n">record_type</span><span class="p">;</span>   <span class="c1">// 0x08 (always 1)</span>
    <span class="kt">uint64_t</span> <span class="n">inode_id</span><span class="p">;</span>     <span class="c1">// 0x09</span>
    <span class="kt">uint64_t</span> <span class="n">private_id</span><span class="p">;</span>   <span class="c1">// 0x11</span>
<span class="p">}</span> <span class="n">clonegroup_mapping_key_t</span><span class="p">;</span> <span class="c1">// 0x19 (25 bytes, packed)</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">group_id</code>: The clone group identifier</li>
  <li><code class="language-plaintext highlighter-rouge">record_type</code>: Always 1 for mapping records</li>
  <li><code class="language-plaintext highlighter-rouge">inode_id</code>: The inode number of the group member</li>
  <li><code class="language-plaintext highlighter-rouge">private_id</code>: The inode’s data stream identifier (<code class="language-plaintext highlighter-rouge">private_id</code> from <code class="language-plaintext highlighter-rouge">j_inode_val_t</code>)</li>
</ul>

<p>Keys are sorted by <code class="language-plaintext highlighter-rouge">group_id</code>, then <code class="language-plaintext highlighter-rouge">record_type</code>, then <code class="language-plaintext highlighter-rouge">inode_id</code>, then <code class="language-plaintext highlighter-rouge">private_id</code>.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define CLONEGROUP_FLAG_FULL_CLONE     0x10
#define CLONEGROUP_FLAG_PURGEABLE_MASK 0x0F
</span>
<span class="k">typedef</span> <span class="k">struct</span> <span class="nc">clonegroup_val</span> <span class="p">{</span>
    <span class="kt">uint64_t</span> <span class="n">physical_size</span><span class="p">;</span> <span class="c1">// 0x00</span>
    <span class="kt">uint32_t</span> <span class="n">flags</span><span class="p">;</span>         <span class="c1">// 0x08</span>
    <span class="kt">uint8_t</span> <span class="n">xfields</span><span class="p">[];</span>      <span class="c1">// 0x0C</span>
<span class="p">}</span> <span class="n">clonegroup_val_t</span><span class="p">;</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">physical_size</code>: The total physical size in bytes of extents this inode contributes to the group. For the full clone, this equals the on-disk size of all shared extents. For partial clones, this is 0.</li>
  <li><code class="language-plaintext highlighter-rouge">flags</code>: Bit 4 (<code class="language-plaintext highlighter-rouge">CLONEGROUP_FLAG_FULL_CLONE</code>) indicates this inode owns the physical extents. Bits 0-3 encode purgeable urgency.</li>
  <li><code class="language-plaintext highlighter-rouge">xfields</code>: Optional extended fields (same format as inode extended fields)</li>
</ul>

<h3 id="cookie-records-record_type--2">Cookie Records (record_type = 2)</h3>

<p>Cookie records signal that a clone group has been reduced to a single member and can be cleaned up.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">clonegroup_cookie_key</span> <span class="p">{</span>
    <span class="kt">uint64_t</span> <span class="n">group_id</span><span class="p">;</span>    <span class="c1">// 0x00</span>
    <span class="kt">uint8_t</span> <span class="n">record_type</span><span class="p">;</span>  <span class="c1">// 0x08 (always 2)</span>
    <span class="kt">uint64_t</span> <span class="n">cookie</span><span class="p">;</span>      <span class="c1">// 0x09</span>
<span class="p">}</span> <span class="n">clonegroup_cookie_key_t</span><span class="p">;</span> <span class="c1">// 0x11 (17 bytes, packed)</span>
</code></pre></div></div>

<p>Note that the <code class="language-plaintext highlighter-rouge">cookie</code> field in the key is a <code class="language-plaintext highlighter-rouge">uint64_t</code>. The cookie record’s <em>value</em> is separate: it is a single byte set to 0. The record’s presence triggers the solo-group cleanup path.</p>

<h2 id="lifecycle">Lifecycle</h2>

<h3 id="group-creation">Group Creation</h3>

<p>When a file is first cloned and the clone group does not yet exist:</p>

<ol>
  <li>A mapping record is inserted for the source inode with <code class="language-plaintext highlighter-rouge">CLONEGROUP_FLAG_FULL_CLONE</code> set and <code class="language-plaintext highlighter-rouge">physical_size</code> reflecting its data extent size.</li>
  <li><code class="language-plaintext highlighter-rouge">INO_EXT_TYPE_CLONEGROUP_ID</code> is set on the source inode.</li>
  <li>A mapping record is inserted for the clone with <code class="language-plaintext highlighter-rouge">physical_size = 0</code> (partial clone).</li>
  <li><code class="language-plaintext highlighter-rouge">INO_EXT_TYPE_CLONEGROUP_ID</code> is set on the clone.</li>
</ol>

<h3 id="adding-members">Adding Members</h3>

<p>Each subsequent clone of any group member gets its own mapping record as a partial clone. The group grows without any data being physically copied.</p>

<h3 id="full-clone-promotion-and-demotion">Full Clone Promotion and Demotion</h3>

<p>As clones diverge through copy-on-write, an inode’s relationship to the shared extents changes:</p>

<ul>
  <li>When an inode that was a partial clone has fully diverged (all its extents are unique), it becomes a full clone of its own data.</li>
  <li>When a full clone is deleted, ownership of the shared physical extents must transfer to another group member.</li>
</ul>

<p>These transitions are tracked by setting or clearing <code class="language-plaintext highlighter-rouge">CLONEGROUP_FLAG_FULL_CLONE</code> and updating <code class="language-plaintext highlighter-rouge">physical_size</code>.</p>

<h3 id="deletion">Deletion</h3>

<p>When a group member is deleted:</p>

<ol>
  <li>Its mapping record is removed from the clonegroup tree.</li>
  <li>If the deleted inode was the full clone, ownership transfers to another member.</li>
  <li>If only one member remains, a cookie record is inserted to mark the group for cleanup.</li>
</ol>

<h3 id="solo-group-cleanup">Solo Group Cleanup</h3>

<p>When a group is reduced to a single member, the clone group tracking overhead is no longer needed. The cleanup process removes the remaining mapping record, the cookie record, and the <code class="language-plaintext highlighter-rouge">INO_EXT_TYPE_CLONEGROUP_ID</code> extended field from the surviving inode.</p>

<h2 id="forensic-considerations">Forensic Considerations</h2>

<p>The clonegroup tree provides insight into file relationships that cannot be derived from extent records alone:</p>

<ul>
  <li>It reveals which files were created by cloning, even after copy-on-write has caused their extents to partially or fully diverge.</li>
  <li>The <code class="language-plaintext highlighter-rouge">physical_size</code> field on the full clone indicates how much shared data exists, which is important for accurate disk space accounting.</li>
  <li>Cookie records reveal clone groups that are in the process of being dissolved.</li>
  <li>The <code class="language-plaintext highlighter-rouge">group_id</code> links related files that may be spread across different directories, enabling reconstruction of clone relationships.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Clonegroups provide the bookkeeping layer above APFS’s extent-level reference counting. While physical extents track shared blocks, clonegroups track shared <em>relationships</em> between files. This enables efficient space accounting, orderly ownership transfer during deletion, and cleanup when clone groups dissolve.</p>]]></content><author><name></name></author><category term="file-systems" /><category term="apfs" /><category term="apfs" /><category term="clonegroups" /><category term="copy-on-write" /><summary type="html"><![CDATA[In our post on Data Streams, we discussed how APFS implements file cloning through shared extents and reference counting. While j_phys_ext_val_t reference counts and j_dstream_id_val_t track sharing at the extent level, APFS also maintains a higher-level grouping mechanism called clonegroups that tracks which inodes share physical data. This post covers the clonegroup tree and its role in managing cloned files.]]></summary></entry><entry><title type="html">Transparent Compression (DECMPFS)</title><link href="https://jtsylve.blog/post/2026/06/08/APFS-DECMPFS" rel="alternate" type="text/html" title="Transparent Compression (DECMPFS)" /><published>2026-06-08T00:00:00+00:00</published><updated>2026-06-15T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2026/06/08/APFS%20DECMPFS</id><content type="html" xml:base="https://jtsylve.blog/post/2026/06/08/APFS-DECMPFS"><![CDATA[<p>APFS supports transparent file compression through the DECMPFS (Decompression File System) framework, shared with HFS+. Compressed files appear normal to applications but store their data in a compressed form on disk, significantly reducing space usage on system volumes. This post covers the on-disk format, compression types, and how to parse compressed files.</p>

<h2 id="overview">Overview</h2>

<p>A compressed file is identified by the <code class="language-plaintext highlighter-rouge">UF_COMPRESSED</code> BSD flag set in its <a href="/post/2022/12/16/APFS-Inode-and-Directory-Records">inode record</a>. When this flag is present, the file’s actual data is stored in either an <a href="/post/2022/12/19/APFS-Data-Streams">extended attribute</a> named <code class="language-plaintext highlighter-rouge">com.apple.decmpfs</code> (for small files) or in the file’s resource fork (for larger files). The kernel transparently decompresses data on read, so applications never see the compressed form.</p>

<h2 id="the-decmpfs_disk_header">The decmpfs_disk_header</h2>

<p>The <code class="language-plaintext highlighter-rouge">com.apple.decmpfs</code> extended attribute begins with a fixed header:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define DECMPFS_MAGIC 0x636d7066 // 'cmpf'
</span>
<span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="kt">uint32_t</span> <span class="n">compression_magic</span><span class="p">;</span>  <span class="c1">// 0x00</span>
    <span class="kt">uint32_t</span> <span class="n">compression_type</span><span class="p">;</span>   <span class="c1">// 0x04</span>
    <span class="kt">uint64_t</span> <span class="n">uncompressed_size</span><span class="p">;</span>  <span class="c1">// 0x08</span>
    <span class="kt">uint8_t</span> <span class="n">attr_bytes</span><span class="p">[];</span>        <span class="c1">// 0x10</span>
<span class="p">}</span> <span class="n">decmpfs_disk_header</span><span class="p">;</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">compression_magic</code>: Must equal <code class="language-plaintext highlighter-rouge">DECMPFS_MAGIC</code> (<code class="language-plaintext highlighter-rouge">0x636d7066</code>). All fields are little-endian. The literal <code class="language-plaintext highlighter-rouge">0x636d7066</code> spells <code class="language-plaintext highlighter-rouge">'cmpf'</code> as a big-endian integer, but stored little-endian it appears on disk as the byte sequence <code class="language-plaintext highlighter-rouge">66 70 6d 63</code> (<code class="language-plaintext highlighter-rouge">'f'</code>,<code class="language-plaintext highlighter-rouge">'p'</code>,<code class="language-plaintext highlighter-rouge">'m'</code>,<code class="language-plaintext highlighter-rouge">'c'</code>).</li>
  <li><code class="language-plaintext highlighter-rouge">compression_type</code>: Identifies the compression algorithm and data location (see below)</li>
  <li><code class="language-plaintext highlighter-rouge">uncompressed_size</code>: The original uncompressed file size in bytes (for <code class="language-plaintext highlighter-rouge">DATALESS_PKG_CMPFS_TYPE</code> this field is reinterpreted: the low 40 bits hold the package size and the upper bits hold a child-entry count)</li>
  <li><code class="language-plaintext highlighter-rouge">attr_bytes</code>: Inline compressed data (for xattr-stored types), or empty for resource fork types</li>
</ul>

<p>The maximum size of the entire <code class="language-plaintext highlighter-rouge">com.apple.decmpfs</code> extended attribute is 3802 bytes. If the compressed data exceeds this limit, it must be stored in the resource fork.</p>

<h2 id="compression-types">Compression Types</h2>

<table style="margin-left: 0">
  <thead>
    <tr>
      <th>Type</th>
      <th>Algorithm</th>
      <th>Location</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>None</td>
      <td>xattr</td>
      <td>Small files stored uncompressed inline</td>
    </tr>
    <tr>
      <td>3</td>
      <td>zlib</td>
      <td>xattr</td>
      <td>Small zlib-compressed files</td>
    </tr>
    <tr>
      <td>4</td>
      <td>zlib</td>
      <td>resource fork</td>
      <td>Larger zlib-compressed files</td>
    </tr>
    <tr>
      <td>5</td>
      <td>Dataless</td>
      <td>none</td>
      <td>Data fetched on demand (iCloud/network)</td>
    </tr>
    <tr>
      <td>7</td>
      <td>LZVN</td>
      <td>xattr</td>
      <td>Fast LZ77 variant (macOS 10.9+)</td>
    </tr>
    <tr>
      <td>8</td>
      <td>LZVN</td>
      <td>resource fork</td>
      <td>Larger LZVN files</td>
    </tr>
    <tr>
      <td>9</td>
      <td>None</td>
      <td>xattr</td>
      <td>Uncompressed data stored inline</td>
    </tr>
    <tr>
      <td>10</td>
      <td>None</td>
      <td>resource fork</td>
      <td>64KB chunks, uncompressed</td>
    </tr>
    <tr>
      <td>11</td>
      <td>LZFSE</td>
      <td>xattr</td>
      <td>High-efficiency entropy-coded (macOS 10.11+)</td>
    </tr>
    <tr>
      <td>12</td>
      <td>LZFSE</td>
      <td>resource fork</td>
      <td>Larger LZFSE files</td>
    </tr>
    <tr>
      <td>13</td>
      <td>LZBITMAP</td>
      <td>xattr</td>
      <td>Block bitmap compression</td>
    </tr>
    <tr>
      <td>14</td>
      <td>LZBITMAP</td>
      <td>resource fork</td>
      <td>Larger LZBITMAP files</td>
    </tr>
  </tbody>
</table>

<p>Odd-numbered types (3, 7, 9, 11, 13) store data inline in the extended attribute. Even-numbered types (4, 8, 10, 12, 14) store data in the resource fork.</p>

<h3 id="dataless-files">Dataless Files</h3>

<p>Special compression types represent files whose content is not stored locally:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define DATALESS_CMPFS_TYPE     0x80000001
#define DATALESS_PKG_CMPFS_TYPE 0x80000002
</span></code></pre></div></div>

<p>These are placeholders for iCloud-synced or network-mounted content. The metadata (size, permissions) exists locally, but the data is fetched on demand.</p>

<h2 id="parsing-a-compressed-file">Parsing a Compressed File</h2>

<ol>
  <li>Check the <code class="language-plaintext highlighter-rouge">UF_COMPRESSED</code> flag (bit 5 of <code class="language-plaintext highlighter-rouge">bsd_flags</code> in <code class="language-plaintext highlighter-rouge">j_inode_val_t</code>).</li>
  <li>Read the <code class="language-plaintext highlighter-rouge">com.apple.decmpfs</code> extended attribute from the File System Tree.</li>
  <li>Verify <code class="language-plaintext highlighter-rouge">compression_magic</code> equals <code class="language-plaintext highlighter-rouge">DECMPFS_MAGIC</code>.</li>
  <li>Read <code class="language-plaintext highlighter-rouge">compression_type</code> to determine the algorithm and data location.</li>
  <li>Locate the compressed data:
    <ul>
      <li><strong>Inline (types 1, 3, 7, 9, 11, 13):</strong> Data follows the header in <code class="language-plaintext highlighter-rouge">attr_bytes</code>.</li>
      <li><strong>Resource fork (types 4, 8, 10, 12, 14):</strong> Data is in the <code class="language-plaintext highlighter-rouge">com.apple.ResourceFork</code> extended attribute.</li>
    </ul>
  </li>
  <li>Decompress using the appropriate algorithm.</li>
</ol>

<h2 id="resource-fork-chunking">Resource Fork Chunking</h2>

<p>Resource fork compression types split data into 65,536-byte (64 KB) chunks, each compressed independently. The number of chunks is <code class="language-plaintext highlighter-rouge">ceil(uncompressed_size / 65536)</code>. Two chunking schemes exist, selected by <code class="language-plaintext highlighter-rouge">compression_type</code> alone (not by any field in the resource fork):</p>

<h3 id="fixed-offset-scheme-type-4-zlib">Fixed-offset scheme (Type 4, zlib)</h3>

<p>The resource fork begins with a 256-byte classic HFS+ resource-fork header whose big-endian <code class="language-plaintext highlighter-rouge">data_offset</code> equals <code class="language-plaintext highlighter-rouge">0x104</code> (<code class="language-plaintext highlighter-rouge">struct decmpfs_rsrc_chunk_table_fixed</code>). At fixed resource-fork offset <code class="language-plaintext highlighter-rouge">0x104</code> a little-endian <code class="language-plaintext highlighter-rouge">uint32_t</code> chunk count is stored, and at <code class="language-plaintext highlighter-rouge">0x108</code> a chunk table follows: one 8-byte entry per chunk, each a <code class="language-plaintext highlighter-rouge">[offset, length]</code> pair of little-endian <code class="language-plaintext highlighter-rouge">uint32_t</code>s. Each chunk’s <code class="language-plaintext highlighter-rouge">offset</code> is relative to <code class="language-plaintext highlighter-rouge">0x104</code>, so chunk <code class="language-plaintext highlighter-rouge">i</code>’s compressed bytes start at resource-fork byte <code class="language-plaintext highlighter-rouge">0x104 + offset[i]</code> and run for <code class="language-plaintext highlighter-rouge">length[i]</code> bytes. The resource fork ends with a constant 50-byte HFS+ resource-map trailer.</p>

<h3 id="absolute-offset-scheme-types-8-10-12-14">Absolute-offset scheme (Types 8, 10, 12, 14)</h3>

<p>There is no resource-fork header, no stored chunk count, and no <code class="language-plaintext highlighter-rouge">cmpf</code> resource map. At resource-fork byte <code class="language-plaintext highlighter-rouge">0</code>, an array of <code class="language-plaintext highlighter-rouge">num_chunks + 1</code> little-endian 4-byte absolute offsets begins (<code class="language-plaintext highlighter-rouge">struct decmpfs_rsrc_chunk_table_abs</code>), where <code class="language-plaintext highlighter-rouge">num_chunks = ceil(uncompressed_size / 65536)</code>. Read <code class="language-plaintext highlighter-rouge">offsets[i]</code> at byte <code class="language-plaintext highlighter-rouge">4 * i</code>. Chunk <code class="language-plaintext highlighter-rouge">i</code> spans <code class="language-plaintext highlighter-rouge">[offsets[i], offsets[i+1])</code>, so its compressed length is <code class="language-plaintext highlighter-rouge">offsets[i+1] - offsets[i]</code>. The final entry <code class="language-plaintext highlighter-rouge">offsets[num_chunks]</code> equals the total resource-fork size.</p>

<h3 id="stored-chunks">Stored chunks</h3>

<p>In both schemes a chunk that would not shrink under compression is stored uncompressed, flagged by a marker as its first byte. When a chunk’s first byte equals the algorithm’s marker, skip that byte and copy the remaining <code class="language-plaintext highlighter-rouge">length - 1</code> bytes verbatim; otherwise decompress with the type’s algorithm. The markers are zlib/LZFSE/LZBITMAP = <code class="language-plaintext highlighter-rouge">0xFF</code>, LZVN = <code class="language-plaintext highlighter-rouge">0x06</code>, and none (type 10) = <code class="language-plaintext highlighter-rouge">0xCC</code>.</p>

<h2 id="interaction-with-apfs">Interaction with APFS</h2>

<p>When the kernel hides extended attributes from userland for compressed files:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">com.apple.decmpfs</code> is always hidden</li>
  <li><code class="language-plaintext highlighter-rouge">com.apple.ResourceFork</code> is hidden when it contains compression data</li>
</ul>

<p>This means forensic tools accessing raw APFS structures will see these attributes, but tools going through the VFS layer will not. The <code class="language-plaintext highlighter-rouge">INODE_HAS_UNCOMPRESSED_SIZE</code> flag (0x40000) in <code class="language-plaintext highlighter-rouge">internal_flags</code> indicates the inode’s <code class="language-plaintext highlighter-rouge">uncompressed_size</code> field is valid.</p>

<p>On <a href="/post/2022/12/20/APFS-Sealed-Volumes">sealed volumes</a>, compressed data integrity is verified through the sealed volume’s hash tree. The <code class="language-plaintext highlighter-rouge">apfs_verify_uncompressed_data</code> function checks decompressed blocks against expected hashes.</p>

<h2 id="forensic-considerations">Forensic Considerations</h2>

<ul>
  <li>Transparent compression is extremely common on macOS system volumes. Most files in <code class="language-plaintext highlighter-rouge">/System</code> and <code class="language-plaintext highlighter-rouge">/usr</code> are compressed.</li>
  <li>The reported file size (in the inode) is the <em>compressed</em> size (allocated extents). The <em>actual</em> size is in <code class="language-plaintext highlighter-rouge">uncompressed_size</code> from the decmpfs header or the inode’s extended field.</li>
  <li>Tools that read raw disk data must handle decompression to access file contents.</li>
  <li>The compression type reveals which macOS version created the file: LZVN (10.9+), LZFSE (10.11+), LZBITMAP (macOS 11+).</li>
  <li>Dataless files (types 0x80000001, 0x80000002) indicate cloud-synced content whose data was never stored locally or has been evicted.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>DECMPFS provides transparent, per-file compression that is deeply integrated into APFS through extended attributes and resource forks. Understanding the compression types and chunking schemes is essential for any tool that needs to access file contents on APFS volumes, particularly system volumes where compression is the default.</p>]]></content><author><name></name></author><category term="file-systems" /><category term="apfs" /><category term="apfs" /><category term="compression" /><category term="decmpfs" /><summary type="html"><![CDATA[APFS supports transparent file compression through the DECMPFS (Decompression File System) framework, shared with HFS+. Compressed files appear normal to applications but store their data in a compressed form on disk, significantly reducing space usage on system volumes. This post covers the on-disk format, compression types, and how to parse compressed files.]]></summary></entry><entry><title type="html">Hard Links and Siblings</title><link href="https://jtsylve.blog/post/2026/06/05/APFS-Siblings" rel="alternate" type="text/html" title="Hard Links and Siblings" /><published>2026-06-05T00:00:00+00:00</published><updated>2026-06-05T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2026/06/05/APFS%20Siblings</id><content type="html" xml:base="https://jtsylve.blog/post/2026/06/05/APFS-Siblings"><![CDATA[<p>In our <a href="/post/2022/12/16/APFS-Inode-and-Directory-Records">post on Inode and Directory Records</a>, we noted that a single inode may be referenced by more than one directory record, as is the case with hard links. In <a href="/post/2022/12/15/APFS-FSTrees">File System Trees</a>, we listed <code class="language-plaintext highlighter-rouge">APFS_TYPE_SIBLING_LINK</code> and <code class="language-plaintext highlighter-rouge">APFS_TYPE_SIBLING_MAP</code> among the record types. Today we examine how APFS explicitly tracks hard links through a mechanism called <em>siblings</em>.</p>

<h2 id="why-siblings-exist">Why Siblings Exist</h2>

<p>Traditional Unix file systems track hard links implicitly: an inode has a link count (<code class="language-plaintext highlighter-rouge">nlink</code>), and each directory entry pointing to it constitutes a link. There is no built-in way to enumerate all the names of a hard-linked file without scanning the entire file system.</p>

<p>APFS tracks hard links explicitly. Each hard link to an inode is called a <em>sibling</em> and is assigned its own unique identifier. This enables:</p>
<ul>
  <li>Efficient enumeration of all names for a file</li>
  <li>Bidirectional mapping between sibling identifiers and inodes</li>
  <li>Support for macOS Carbon APIs that require distinguishing between links to the same file</li>
  <li>Proper Spotlight indexing and file coordination across multiple names</li>
</ul>

<p>The sibling with the lowest identifier is the <em>primary link</em>. The inode’s <code class="language-plaintext highlighter-rouge">parent_id</code> and <code class="language-plaintext highlighter-rouge">INO_EXT_TYPE_NAME</code> extended field always reflect the primary link’s parent directory and name.</p>

<h2 id="sibling-link-records">Sibling Link Records</h2>

<p><em>Sibling link records</em> (type <code class="language-plaintext highlighter-rouge">APFS_TYPE_SIBLING_LINK</code>) map from an inode to each of its hard links. They are stored in the <a href="/post/2022/12/15/APFS-FSTrees">File System Tree</a>.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">j_sibling_key</span> <span class="p">{</span>
    <span class="n">j_key_t</span> <span class="n">hdr</span><span class="p">;</span>          <span class="c1">// 0x00</span>
    <span class="kt">uint64_t</span> <span class="n">sibling_id</span><span class="p">;</span>  <span class="c1">// 0x08</span>
<span class="p">}</span> <span class="n">j_sibling_key_t</span><span class="p">;</span>        <span class="c1">// 0x10</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">hdr</code>: The record’s header. The object identifier is the inode number.</li>
  <li><code class="language-plaintext highlighter-rouge">sibling_id</code>: The sibling’s unique identifier</li>
</ul>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">j_sibling_val</span> <span class="p">{</span>
    <span class="kt">uint64_t</span> <span class="n">parent_id</span><span class="p">;</span>  <span class="c1">// 0x00</span>
    <span class="kt">uint16_t</span> <span class="n">name_len</span><span class="p">;</span>   <span class="c1">// 0x08</span>
    <span class="kt">uint8_t</span> <span class="n">name</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>     <span class="c1">// 0x0A</span>
<span class="p">}</span> <span class="n">j_sibling_val_t</span><span class="p">;</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">parent_id</code>: The inode number of the parent directory containing this link</li>
  <li><code class="language-plaintext highlighter-rouge">name_len</code>: The length of the name including the null terminator</li>
  <li><code class="language-plaintext highlighter-rouge">name</code>: The null-terminated UTF-8 name of the directory entry</li>
</ul>

<p>For a file with three hard links, there will be three sibling link records, all sharing the same inode number in their key header but each with a unique <code class="language-plaintext highlighter-rouge">sibling_id</code>. Each record stores the parent directory and name for that particular link.</p>

<h2 id="sibling-map-records">Sibling Map Records</h2>

<p><em>Sibling map records</em> (type <code class="language-plaintext highlighter-rouge">APFS_TYPE_SIBLING_MAP</code>) provide the reverse mapping: given a sibling identifier, find the inode.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">j_sibling_map_key</span> <span class="p">{</span>
    <span class="n">j_key_t</span> <span class="n">hdr</span><span class="p">;</span> <span class="c1">// 0x00</span>
<span class="p">}</span> <span class="n">j_sibling_map_key_t</span><span class="p">;</span> <span class="c1">// 0x08</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">hdr</code>: The record’s header. The object identifier is the sibling’s unique identifier.</li>
</ul>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">j_sibling_map_val</span> <span class="p">{</span>
    <span class="kt">uint64_t</span> <span class="n">file_id</span><span class="p">;</span> <span class="c1">// 0x00</span>
<span class="p">}</span> <span class="n">j_sibling_map_val_t</span><span class="p">;</span> <span class="c1">// 0x08</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">file_id</code>: The inode number of the underlying file</li>
</ul>

<p>This bidirectional mapping (sibling link: inode -&gt; sibling ID + location; sibling map: sibling ID -&gt; inode) allows efficient traversal in either direction.</p>

<h2 id="sibling-identifier-allocation">Sibling Identifier Allocation</h2>

<p>Sibling identifiers are allocated from the same object identifier space as inode numbers (from the volume’s <code class="language-plaintext highlighter-rouge">next_obj_id</code> counter). Each directory record for a hard-linked file stores its sibling identifier in the <code class="language-plaintext highlighter-rouge">DREC_EXT_TYPE_SIBLING_ID</code> extended field, linking the directory entry to its corresponding sibling records.</p>

<h2 id="operations">Operations</h2>

<p>When the first hard link is created (the target’s <code class="language-plaintext highlighter-rouge">nlink</code> is still 1 and its existing directory entry has no <code class="language-plaintext highlighter-rouge">DREC_EXT_TYPE_SIBLING_ID</code> field), the original entry is first promoted to a sibling: a sibling identifier is allocated for it, a <code class="language-plaintext highlighter-rouge">DREC_EXT_TYPE_SIBLING_ID</code> field is added to that existing directory entry, and sibling link and map records are created for the original link. The steps below then run for the new link.</p>

<p>When a hard link is created:</p>
<ol>
  <li>A new sibling identifier is allocated from <code class="language-plaintext highlighter-rouge">next_obj_id</code> for the new link (on the first hard link, a second identifier is also allocated to promote the original entry; see above).</li>
  <li>A sibling link record is inserted into the File System Tree, keyed by the target inode number and the new sibling ID.</li>
  <li>A sibling map record is inserted, keyed by the sibling ID, with the target inode as the value.</li>
  <li>The directory record receives a <code class="language-plaintext highlighter-rouge">DREC_EXT_TYPE_SIBLING_ID</code> extended field with the sibling ID.</li>
  <li>Because sibling identifiers are handed out in increasing order from <code class="language-plaintext highlighter-rouge">next_obj_id</code>, a newly created link always has a higher identifier than every existing sibling, so creating a link never changes which sibling is the primary link.</li>
</ol>

<p>When a hard link is removed:</p>
<ol>
  <li>Both the sibling link record and sibling map record are deleted.</li>
  <li>If the removed link was the primary link, the inode’s metadata is updated to reflect the next-lowest sibling as the new primary.</li>
</ol>

<h2 id="hard-link-fixup-at-mount">Hard-Link Fixup at Mount</h2>

<p>On volumes where the <code class="language-plaintext highlighter-rouge">APFS_FEATURE_HARDLINK_MAP_RECORDS</code> feature flag (bit 1 of <code class="language-plaintext highlighter-rouge">apfs_features</code>) is not set, the implementation runs a fixup pass at mount time. This pass iterates all <code class="language-plaintext highlighter-rouge">APFS_TYPE_SIBLING_LINK</code> records and ensures a corresponding <code class="language-plaintext highlighter-rouge">APFS_TYPE_SIBLING_MAP</code> record exists for each one. Progress is tracked via the <code class="language-plaintext highlighter-rouge">fixup-hardlink-progress</code> extended attribute on the root directory (inode 2), which stores the last processed object identifier.</p>

<p>Once fixup completes, <code class="language-plaintext highlighter-rouge">APFS_FEATURE_HARDLINK_MAP_RECORDS</code> is set and the progress attribute is removed. This mechanism handles the transition from older APFS implementations that did not maintain sibling map records.</p>

<h2 id="forensic-considerations">Forensic Considerations</h2>

<p>Sibling records are valuable for forensic analysis:</p>

<ul>
  <li>They allow complete enumeration of all paths to a file without scanning every directory entry on the volume.</li>
  <li>The <code class="language-plaintext highlighter-rouge">parent_id</code> in sibling link records reveals which directories contain links to a file, even if some of those directory entries have been deleted or are in snapshots.</li>
  <li>Inconsistencies between sibling records and directory entries (or between sibling link and sibling map records) may indicate tampering or corruption.</li>
  <li>The <code class="language-plaintext highlighter-rouge">DREC_EXT_TYPE_SIBLING_ID</code> extended field in directory records provides a cross-reference that can validate the integrity of the sibling mapping.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>APFS’s explicit hard link tracking through sibling records distinguishes it from traditional Unix file systems. The bidirectional mapping between inodes and sibling identifiers enables efficient enumeration, correct primary link tracking, and robust support for macOS APIs that distinguish between names of the same file.</p>]]></content><author><name></name></author><category term="file-systems" /><category term="apfs" /><category term="apfs" /><category term="hard-links" /><category term="siblings" /><summary type="html"><![CDATA[In our post on Inode and Directory Records, we noted that a single inode may be referenced by more than one directory record, as is the case with hard links. In File System Trees, we listed APFS_TYPE_SIBLING_LINK and APFS_TYPE_SIBLING_MAP among the record types. Today we examine how APFS explicitly tracks hard links through a mechanism called siblings.]]></summary></entry><entry><title type="html">EFI Jumpstart</title><link href="https://jtsylve.blog/post/2026/06/04/APFS-EFI-Jumpstart" rel="alternate" type="text/html" title="EFI Jumpstart" /><published>2026-06-04T00:00:00+00:00</published><updated>2026-06-04T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2026/06/04/APFS%20EFI%20Jumpstart</id><content type="html" xml:base="https://jtsylve.blog/post/2026/06/04/APFS-EFI-Jumpstart"><![CDATA[<p>APFS containers include an embedded EFI driver that allows UEFI firmware to boot from APFS partitions without requiring a built-in APFS driver. This post covers the <code class="language-plaintext highlighter-rouge">nx_efi_jumpstart_t</code> structure and the boot procedure that uses it.</p>

<h2 id="overview">Overview</h2>

<p>The EFI jumpstart mechanism is intentionally simple. The driver can be located by reading a few data structures starting from physical block zero, without any B-Tree traversal or complex APFS parsing. This minimal dependency means that UEFI firmware (or virtualization software) can load the APFS driver with only basic block-read capability.</p>

<p>The <code class="language-plaintext highlighter-rouge">nx_efi_jumpstart</code> field of the <a href="/post/2022/12/06/APFS-NX-Superblock">NX Superblock</a> stores the physical block address of the jumpstart structure. This field is written during container creation and is not used by the kernel APFS driver during normal operation.</p>

<h2 id="boot-procedure">Boot Procedure</h2>

<p>To boot from an APFS partition using the embedded EFI driver:</p>

<ol>
  <li>
    <p>Read physical block zero (the container superblock). Verify the Fletcher-64 checksum and confirm <code class="language-plaintext highlighter-rouge">nx_magic</code> equals <code class="language-plaintext highlighter-rouge">NX_MAGIC</code> (<code class="language-plaintext highlighter-rouge">'BSXN'</code>).</p>
  </li>
  <li>
    <p>Read the physical block at the address in <code class="language-plaintext highlighter-rouge">nx_efi_jumpstart</code>.</p>
  </li>
  <li>
    <p>Verify <code class="language-plaintext highlighter-rouge">nej_magic</code> equals <code class="language-plaintext highlighter-rouge">NX_EFI_JUMPSTART_MAGIC</code> (<code class="language-plaintext highlighter-rouge">'RDSJ'</code>), verify the Fletcher-64 checksum, and confirm <code class="language-plaintext highlighter-rouge">nej_version</code> is 1.</p>
  </li>
  <li>
    <p>Allocate a contiguous memory buffer of at least <code class="language-plaintext highlighter-rouge">nej_efi_file_len</code> bytes.</p>
  </li>
  <li>
    <p>Read the <code class="language-plaintext highlighter-rouge">nej_num_extents</code> extent records from <code class="language-plaintext highlighter-rouge">nej_rec_extents</code> and load each extent’s blocks sequentially into the memory buffer.</p>
  </li>
  <li>
    <p>Execute the loaded EFI driver.</p>
  </li>
</ol>

<h2 id="nx_efi_jumpstart_t">nx_efi_jumpstart_t</h2>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define NX_EFI_JUMPSTART_MAGIC 'RDSJ'
#define NX_EFI_JUMPSTART_VERSION 1
</span>
<span class="k">typedef</span> <span class="k">struct</span> <span class="nc">nx_efi_jumpstart</span> <span class="p">{</span>
    <span class="n">obj_phys_t</span> <span class="n">nej_o</span><span class="p">;</span>             <span class="c1">// 0x00</span>
    <span class="kt">uint32_t</span> <span class="n">nej_magic</span><span class="p">;</span>           <span class="c1">// 0x20</span>
    <span class="kt">uint32_t</span> <span class="n">nej_version</span><span class="p">;</span>         <span class="c1">// 0x24</span>
    <span class="kt">uint32_t</span> <span class="n">nej_efi_file_len</span><span class="p">;</span>    <span class="c1">// 0x28</span>
    <span class="kt">uint32_t</span> <span class="n">nej_num_extents</span><span class="p">;</span>     <span class="c1">// 0x2C</span>
    <span class="kt">uint64_t</span> <span class="n">nej_reserved</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span>    <span class="c1">// 0x30</span>
    <span class="n">prange_t</span> <span class="n">nej_rec_extents</span><span class="p">[];</span>   <span class="c1">// 0xB0</span>
<span class="p">}</span> <span class="n">nx_efi_jumpstart_t</span><span class="p">;</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">nej_o</code>: The object header (type <code class="language-plaintext highlighter-rouge">OBJECT_TYPE_EFI_JUMPSTART</code>, physical)</li>
  <li><code class="language-plaintext highlighter-rouge">nej_magic</code>: Must equal <code class="language-plaintext highlighter-rouge">NX_EFI_JUMPSTART_MAGIC</code> (<code class="language-plaintext highlighter-rouge">'RDSJ'</code>, on-disk bytes <code class="language-plaintext highlighter-rouge">4A 53 44 52</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">nej_version</code>: Must equal 1</li>
  <li><code class="language-plaintext highlighter-rouge">nej_efi_file_len</code>: The total size of the embedded EFI driver in bytes</li>
  <li><code class="language-plaintext highlighter-rouge">nej_num_extents</code>: The number of physical extent records that follow</li>
  <li><code class="language-plaintext highlighter-rouge">nej_reserved</code>: Reserved (128 bytes, set to zero)</li>
  <li><code class="language-plaintext highlighter-rouge">nej_rec_extents</code>: A variable-length array of <code class="language-plaintext highlighter-rouge">prange_t</code> records describing where the EFI driver blocks are stored on disk</li>
</ul>

<p>Each <code class="language-plaintext highlighter-rouge">prange_t</code> in the extent array specifies a starting physical address and a block count:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">prange</span> <span class="p">{</span>
    <span class="n">paddr_t</span> <span class="n">pr_start_paddr</span><span class="p">;</span> <span class="c1">// 0x00</span>
    <span class="kt">uint64_t</span> <span class="n">pr_block_count</span><span class="p">;</span> <span class="c1">// 0x08</span>
<span class="p">}</span> <span class="n">prange_t</span><span class="p">;</span>                  <span class="c1">// 0x10</span>
</code></pre></div></div>

<p>The extents must be read sequentially and concatenated to assemble the complete driver image.</p>

<h2 id="gpt-partition-type">GPT Partition Type</h2>

<p>APFS partitions are identified in the GUID Partition Table by the following type UUID:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>7C3457EF-0000-11AA-AA11-00306543ECAC
</code></pre></div></div>

<p>UEFI firmware uses this UUID to identify partitions that may contain an APFS container with an embedded EFI driver.</p>

<h2 id="forensic-considerations">Forensic Considerations</h2>

<p>The EFI jumpstart structure is useful for forensic validation:</p>

<ul>
  <li>Its presence and validity confirm that the partition was formatted as APFS (as opposed to being partially overwritten).</li>
  <li>The driver extents reference physical blocks that should be within the container’s bounds. Out-of-range addresses indicate corruption.</li>
  <li>The jumpstart structure is independent of checkpoints. Since it is only written during container creation, it provides a stable reference point that survives checkpoint-level corruption.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>The EFI jumpstart mechanism provides a minimal, self-contained boot path for APFS containers. Its simplicity (a single physical object with direct extent references) ensures that UEFI firmware can load the APFS driver without implementing any of the complex B-Tree or checkpoint logic that the rest of APFS requires.</p>]]></content><author><name></name></author><category term="file-systems" /><category term="apfs" /><category term="apfs" /><category term="efi" /><category term="boot" /><summary type="html"><![CDATA[APFS containers include an embedded EFI driver that allows UEFI firmware to boot from APFS partitions without requiring a built-in APFS driver. This post covers the nx_efi_jumpstart_t structure and the boot procedure that uses it.]]></summary></entry><entry><title type="html">The Reaper</title><link href="https://jtsylve.blog/post/2026/06/03/APFS-Reaper" rel="alternate" type="text/html" title="The Reaper" /><published>2026-06-03T00:00:00+00:00</published><updated>2026-06-03T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2026/06/03/APFS%20Reaper</id><content type="html" xml:base="https://jtsylve.blog/post/2026/06/03/APFS-Reaper"><![CDATA[<p>In our <a href="/post/2022/12/05/APFS-Containers">post on Containers</a>, we introduced the Reaper as the subsystem responsible for garbage collection in APFS. The Reaper handles deletions that are too large to complete within a single transaction, such as deleting an entire volume or cleaning up after a snapshot deletion. In this post, we will examine the Reaper’s on-disk structures and its multi-phase state machine.</p>

<h2 id="overview">Overview</h2>

<p>Each APFS container has exactly one Reaper, stored as an ephemeral object in the checkpoint data area. Its object identifier is recorded in the <code class="language-plaintext highlighter-rouge">nx_reaper_oid</code> field of the <a href="/post/2022/12/06/APFS-NX-Superblock">NX Superblock</a>. The Reaper runs in a dedicated kernel thread with throttled I/O priority, processing entries from a linked list of <em>reap list blocks</em>. When a handler cannot complete its work within a single transaction, it saves its progress in a state buffer and resumes in a new transaction.</p>

<h2 id="nx_reaper_phys_t">nx_reaper_phys_t</h2>

<p>The top-level Reaper structure.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">nx_reaper_phys</span> <span class="p">{</span>
    <span class="n">obj_phys_t</span> <span class="n">nr_o</span><span class="p">;</span>              <span class="c1">// 0x00</span>
    <span class="kt">uint64_t</span> <span class="n">nr_next_reap_id</span><span class="p">;</span>    <span class="c1">// 0x20</span>
    <span class="kt">uint64_t</span> <span class="n">nr_completed_id</span><span class="p">;</span>    <span class="c1">// 0x28</span>
    <span class="n">oid_t</span> <span class="n">nr_head</span><span class="p">;</span>               <span class="c1">// 0x30</span>
    <span class="n">oid_t</span> <span class="n">nr_tail</span><span class="p">;</span>               <span class="c1">// 0x38</span>
    <span class="kt">uint32_t</span> <span class="n">nr_flags</span><span class="p">;</span>           <span class="c1">// 0x40</span>
    <span class="kt">uint32_t</span> <span class="n">nr_rlcount</span><span class="p">;</span>         <span class="c1">// 0x44</span>
    <span class="kt">uint32_t</span> <span class="n">nr_type</span><span class="p">;</span>            <span class="c1">// 0x48</span>
    <span class="kt">uint32_t</span> <span class="n">nr_size</span><span class="p">;</span>            <span class="c1">// 0x4C</span>
    <span class="n">oid_t</span> <span class="n">nr_fs_oid</span><span class="p">;</span>             <span class="c1">// 0x50</span>
    <span class="n">oid_t</span> <span class="n">nr_oid</span><span class="p">;</span>                <span class="c1">// 0x58</span>
    <span class="n">xid_t</span> <span class="n">nr_xid</span><span class="p">;</span>                <span class="c1">// 0x60</span>
    <span class="kt">uint32_t</span> <span class="n">nr_nrle_flags</span><span class="p">;</span>      <span class="c1">// 0x68</span>
    <span class="kt">uint32_t</span> <span class="n">nr_state_buffer_size</span><span class="p">;</span> <span class="c1">// 0x6C</span>
    <span class="kt">uint8_t</span> <span class="n">nr_state_buffer</span><span class="p">[];</span>   <span class="c1">// 0x70</span>
<span class="p">}</span> <span class="n">nx_reaper_phys_t</span><span class="p">;</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">nr_o</code>: The object header (type <code class="language-plaintext highlighter-rouge">OBJECT_TYPE_NX_REAPER</code>, ephemeral)</li>
  <li><code class="language-plaintext highlighter-rouge">nr_next_reap_id</code>: The identifier to assign to the next reap operation (initialized to 1)</li>
  <li><code class="language-plaintext highlighter-rouge">nr_completed_id</code>: The identifier of the most recently completed reap operation</li>
  <li><code class="language-plaintext highlighter-rouge">nr_head</code>: Object identifier of the first reap list block (zero if empty)</li>
  <li><code class="language-plaintext highlighter-rouge">nr_tail</code>: Object identifier of the last reap list block (zero if empty)</li>
  <li><code class="language-plaintext highlighter-rouge">nr_flags</code>: Reaper state flags (see below)</li>
  <li><code class="language-plaintext highlighter-rouge">nr_rlcount</code>: Number of reap list blocks in the chain</li>
  <li><code class="language-plaintext highlighter-rouge">nr_type</code>: The object type currently being reaped</li>
  <li><code class="language-plaintext highlighter-rouge">nr_size</code>: Size parameter for the current reap operation</li>
  <li><code class="language-plaintext highlighter-rouge">nr_fs_oid</code>: The volume object identifier associated with the current reap</li>
  <li><code class="language-plaintext highlighter-rouge">nr_oid</code>: Object identifier of the object being reaped (zero when idle)</li>
  <li><code class="language-plaintext highlighter-rouge">nr_xid</code>: Transaction identifier for the current operation</li>
  <li><code class="language-plaintext highlighter-rouge">nr_nrle_flags</code>: Flags from the reap list entry being processed</li>
  <li><code class="language-plaintext highlighter-rouge">nr_state_buffer_size</code>: Size of the state buffer in bytes</li>
  <li><code class="language-plaintext highlighter-rouge">nr_state_buffer</code>: Variable-length buffer for handler progress state</li>
</ul>

<p>The state buffer allows reap handlers to save their position across transaction boundaries. For a 4096-byte block, this buffer is 3984 bytes.</p>

<h4 id="reaper-flags">Reaper Flags</h4>

<table style="margin-left: 0">
  <thead>
    <tr>
      <th>Name</th>
      <th>Value</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>NR_BHM_FLAG</td>
      <td>0x00000001</td>
      <td>Must always be set (initialization flag)</td>
    </tr>
    <tr>
      <td>NR_CONTINUE</td>
      <td>0x00000002</td>
      <td>An object is partially reaped and requires continued processing</td>
    </tr>
  </tbody>
</table>

<h2 id="reap-lists">Reap Lists</h2>

<p>Reap list blocks form a singly linked list from <code class="language-plaintext highlighter-rouge">nr_head</code> to <code class="language-plaintext highlighter-rouge">nr_tail</code>. Each block contains an array of entries describing objects to be reaped.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">nx_reap_list_phys</span> <span class="p">{</span>
    <span class="n">obj_phys_t</span> <span class="n">nrl_o</span><span class="p">;</span>                <span class="c1">// 0x00</span>
    <span class="n">oid_t</span> <span class="n">nrl_next</span><span class="p">;</span>                  <span class="c1">// 0x20</span>
    <span class="kt">uint32_t</span> <span class="n">nrl_flags</span><span class="p">;</span>              <span class="c1">// 0x28</span>
    <span class="kt">uint32_t</span> <span class="n">nrl_max</span><span class="p">;</span>                <span class="c1">// 0x2C</span>
    <span class="kt">uint32_t</span> <span class="n">nrl_count</span><span class="p">;</span>              <span class="c1">// 0x30</span>
    <span class="kt">uint32_t</span> <span class="n">nrl_first</span><span class="p">;</span>             <span class="c1">// 0x34</span>
    <span class="kt">uint32_t</span> <span class="n">nrl_last</span><span class="p">;</span>              <span class="c1">// 0x38</span>
    <span class="kt">uint32_t</span> <span class="n">nrl_free</span><span class="p">;</span>              <span class="c1">// 0x3C</span>
    <span class="n">nx_reap_list_entry_t</span> <span class="n">nrl_entries</span><span class="p">[];</span> <span class="c1">// 0x40</span>
<span class="p">}</span> <span class="n">nx_reap_list_phys_t</span><span class="p">;</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">nrl_o</code>: The object header (type <code class="language-plaintext highlighter-rouge">OBJECT_TYPE_NX_REAP_LIST</code>, ephemeral)</li>
  <li><code class="language-plaintext highlighter-rouge">nrl_next</code>: Object identifier of the next reap list block in the chain, or zero</li>
  <li><code class="language-plaintext highlighter-rouge">nrl_flags</code>: Reserved</li>
  <li><code class="language-plaintext highlighter-rouge">nrl_max</code>: Maximum number of entries (calculated as <code class="language-plaintext highlighter-rouge">(block_size - 64) / 40</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">nrl_count</code>: Number of active entries</li>
  <li><code class="language-plaintext highlighter-rouge">nrl_first</code>: Index of the first active entry, or <code class="language-plaintext highlighter-rouge">0xFFFFFFFF</code> if empty</li>
  <li><code class="language-plaintext highlighter-rouge">nrl_last</code>: Index of the last active entry, or <code class="language-plaintext highlighter-rouge">0xFFFFFFFF</code> if empty</li>
  <li><code class="language-plaintext highlighter-rouge">nrl_free</code>: Index of the first free entry slot, or <code class="language-plaintext highlighter-rouge">0xFFFFFFFF</code> if full</li>
</ul>

<p>Within each block, two linked lists are threaded through the entry array using index chains: an active list of entries awaiting processing and a free list of available slots.</p>

<h3 id="nx_reap_list_entry_t">nx_reap_list_entry_t</h3>

<p>Each entry describes a single object to be reaped.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">nx_reap_list_entry</span> <span class="p">{</span>
    <span class="kt">uint32_t</span> <span class="n">nrle_next</span><span class="p">;</span>   <span class="c1">// 0x00</span>
    <span class="kt">uint32_t</span> <span class="n">nrle_flags</span><span class="p">;</span>  <span class="c1">// 0x04</span>
    <span class="kt">uint32_t</span> <span class="n">nrle_type</span><span class="p">;</span>   <span class="c1">// 0x08</span>
    <span class="kt">uint32_t</span> <span class="n">nrle_size</span><span class="p">;</span>   <span class="c1">// 0x0C</span>
    <span class="n">oid_t</span> <span class="n">nrle_fs_oid</span><span class="p">;</span>    <span class="c1">// 0x10</span>
    <span class="n">oid_t</span> <span class="n">nrle_oid</span><span class="p">;</span>       <span class="c1">// 0x18</span>
    <span class="n">xid_t</span> <span class="n">nrle_xid</span><span class="p">;</span>       <span class="c1">// 0x20</span>
<span class="p">}</span> <span class="n">nx_reap_list_entry_t</span><span class="p">;</span>   <span class="c1">// 0x28</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">nrle_next</code>: Index of the next entry in the chain, or <code class="language-plaintext highlighter-rouge">0xFFFFFFFF</code></li>
  <li><code class="language-plaintext highlighter-rouge">nrle_flags</code>: Entry flags (see below)</li>
  <li><code class="language-plaintext highlighter-rouge">nrle_type</code>: The object type to reap</li>
  <li><code class="language-plaintext highlighter-rouge">nrle_size</code>: Size parameter for the handler</li>
  <li><code class="language-plaintext highlighter-rouge">nrle_fs_oid</code>: Volume object identifier (zero for container-level objects)</li>
  <li><code class="language-plaintext highlighter-rouge">nrle_oid</code>: Object identifier of the object to reap</li>
  <li><code class="language-plaintext highlighter-rouge">nrle_xid</code>: Transaction or reap identifier</li>
</ul>

<h4 id="reap-list-entry-flags">Reap List Entry Flags</h4>

<table style="margin-left: 0">
  <thead>
    <tr>
      <th>Name</th>
      <th>Value</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>NRLE_VALID</td>
      <td>0x00000001</td>
      <td>The entry contains valid data</td>
    </tr>
    <tr>
      <td>NRLE_REAP_ID_RECORD</td>
      <td>0x00000002</td>
      <td>Triggers a completion notification (updates <code class="language-plaintext highlighter-rouge">nr_completed_id</code>)</td>
    </tr>
    <tr>
      <td>NRLE_CALL</td>
      <td>0x00000004</td>
      <td>Triggers the reap handler for the specified object</td>
    </tr>
    <tr>
      <td>NRLE_COMPLETION</td>
      <td>0x00000008</td>
      <td>Marks the entry as a post-reap completion callback</td>
    </tr>
    <tr>
      <td>NRLE_CLEANUP</td>
      <td>0x00000010</td>
      <td>Triggers cleanup operations after reaping</td>
    </tr>
  </tbody>
</table>

<p>When an object is added to the Reaper, two entries are typically appended: a <em>call entry</em> (<code class="language-plaintext highlighter-rouge">NRLE_VALID | NRLE_CALL</code>) that triggers the type-specific handler, and a <em>completion entry</em> (<code class="language-plaintext highlighter-rouge">NRLE_VALID | NRLE_REAP_ID_RECORD</code>) that updates <code class="language-plaintext highlighter-rouge">nr_completed_id</code> when processed. Sub-object entries (such as a volume’s object map during volume deletion) are inserted at the head so they are processed before their parent.</p>

<h2 id="volume-deletion-phases">Volume Deletion Phases</h2>

<p>The most complex reap operation is deleting an entire volume. This proceeds through a sequence of phases (beginning at <code class="language-plaintext highlighter-rouge">APFS_REAP_PHASE_START</code> = 0, which transitions immediately to the snapshot phase), tracked in an <code class="language-plaintext highlighter-rouge">apfs_reap_state_t</code> stored in <code class="language-plaintext highlighter-rouge">nr_state_buffer</code>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">apfs_reap_state</span> <span class="p">{</span>
    <span class="kt">uint64_t</span> <span class="n">last_pbn</span><span class="p">;</span>    <span class="c1">// 0x00</span>
    <span class="n">xid_t</span> <span class="n">cur_snap_xid</span><span class="p">;</span>   <span class="c1">// 0x08</span>
    <span class="kt">uint32_t</span> <span class="n">phase</span><span class="p">;</span>       <span class="c1">// 0x10</span>
<span class="p">}</span> <span class="n">__attribute__</span><span class="p">((</span><span class="n">packed</span><span class="p">))</span> <span class="n">apfs_reap_state_t</span><span class="p">;</span>     <span class="c1">// 0x14 (packed, no padding)</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">last_pbn</code>: Physical block number where extent reaping last paused</li>
  <li><code class="language-plaintext highlighter-rouge">cur_snap_xid</code>: Transaction identifier of the snapshot currently being reaped</li>
  <li><code class="language-plaintext highlighter-rouge">phase</code>: Current deletion phase (0-4)</li>
</ul>

<h3 id="phase-1-apfs_reap_phase_snapshots">Phase 1: APFS_REAP_PHASE_SNAPSHOTS</h3>

<p>All snapshots belonging to the volume are reaped. The Reaper iterates through each snapshot’s extent reference tree, freeing physical extents. Progress is tracked by <code class="language-plaintext highlighter-rouge">cur_snap_xid</code>. Each snapshot’s extentref tree is then deleted, exactly as in normal <a href="/post/2022/12/28/APFS-Snapshot-Metadata">snapshot deletion</a>.</p>

<h3 id="phase-2-apfs_reap_phase_active_fs">Phase 2: APFS_REAP_PHASE_ACTIVE_FS</h3>

<p>After all snapshots are gone, the active file system’s extents are freed. The Reaper walks the volume’s extent reference tree and frees all data extents owned by the volume. Progress is tracked by <code class="language-plaintext highlighter-rouge">last_pbn</code>. Supplemental trees are also destroyed: the sealed volume’s file extent tree (<code class="language-plaintext highlighter-rouge">apfs_fext_tree_oid</code>, present when <code class="language-plaintext highlighter-rouge">APFS_INCOMPAT_SEALED_VOLUME</code> is set) and the per-file key upgrade/rotation tree (<code class="language-plaintext highlighter-rouge">apfs_pfkur_tree_oid</code>, present when <code class="language-plaintext highlighter-rouge">APFS_INCOMPAT_PFK_UPGRADE_ROTATION</code> is set).</p>

<h3 id="phase-3-apfs_reap_phase_destroy_omap">Phase 3: APFS_REAP_PHASE_DESTROY_OMAP</h3>

<p>The volume’s <a href="/post/2022/12/12/APFS-OMAP">Object Map</a> is destroyed. This is added to the Reaper as a sub-object, using its own state tracking (<code class="language-plaintext highlighter-rouge">omap_reap_state_t</code>). After the OMAP is fully reaped, crypto state, key caches, and the volume’s superblock metadata are cleared.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">omap_reap_state</span> <span class="p">{</span>
    <span class="kt">uint32_t</span> <span class="n">omr_phase</span><span class="p">;</span>  <span class="c1">// 0x00</span>
    <span class="kt">uint32_t</span> <span class="n">omr_pad</span><span class="p">;</span>    <span class="c1">// 0x04</span>
    <span class="n">omap_key_t</span> <span class="n">omr_ok</span><span class="p">;</span>   <span class="c1">// 0x08</span>
<span class="p">}</span> <span class="n">omap_reap_state_t</span><span class="p">;</span>     <span class="c1">// 0x18</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">omr_phase</code>: Current phase (<code class="language-plaintext highlighter-rouge">OMAP_REAP_PHASE_MAP_TREE</code> = 1, <code class="language-plaintext highlighter-rouge">OMAP_REAP_PHASE_SNAPSHOT_TREE</code> = 2)</li>
  <li><code class="language-plaintext highlighter-rouge">omr_ok</code>: The last freed key, used to resume iteration after a transaction boundary</li>
</ul>

<h3 id="phase-4-apfs_reap_phase_done">Phase 4: APFS_REAP_PHASE_DONE</h3>

<p>All volume structures have been freed. The reap operation is complete.</p>

<h2 id="crash-recovery">Crash Recovery</h2>

<p>The Reaper’s design guarantees crash-safe resumption. If the system crashes mid-reap:</p>

<ol>
  <li>The Reaper’s ephemeral object is restored from the checkpoint. Since <code class="language-plaintext highlighter-rouge">nr_oid</code> is nonzero and <code class="language-plaintext highlighter-rouge">NR_CONTINUE</code> is set in <code class="language-plaintext highlighter-rouge">nr_flags</code>, the Reaper knows to resume.</li>
  <li>On the next mount, the Reaper thread starts and enters a transaction.</li>
  <li>Since <code class="language-plaintext highlighter-rouge">nr_oid</code> is already set, it skips entry dequeue and goes directly to handler dispatch.</li>
  <li>The handler reads its saved state from <code class="language-plaintext highlighter-rouge">nr_state_buffer</code> and resumes where it left off.</li>
</ol>

<p>This ensures that even multi-transaction deletions spanning many checkpoints will always complete, regardless of how many crashes occur during the process.</p>

<h2 id="forensic-considerations">Forensic Considerations</h2>

<p>The Reaper is forensically significant because:</p>

<ul>
  <li><strong>Partially reaped volumes</strong> may still contain recoverable data. The <code class="language-plaintext highlighter-rouge">phase</code> field in the reap state indicates how far deletion has progressed. Data in phases not yet reached may be fully intact.</li>
  <li><strong>The reap list</strong> reveals which objects are pending deletion. A volume that appears missing from the container’s <code class="language-plaintext highlighter-rouge">nx_fs_oid</code> array may still exist in the Reaper’s queue.</li>
  <li><strong>The <code class="language-plaintext highlighter-rouge">nr_completed_id</code> and <code class="language-plaintext highlighter-rouge">nr_next_reap_id</code> fields</strong> provide a history of how many reap operations have occurred, giving insight into container activity.</li>
  <li><strong>Free queue entries</strong> from reaper-freed blocks retain their transaction identifiers, indicating when deletion occurred.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>The Reaper provides crash-safe, multi-transaction garbage collection for APFS. Its state machine design allows arbitrarily large deletions (entire volumes, snapshot cleanup, object map destruction) to proceed incrementally across as many transactions as needed, with guaranteed resumption after crashes. Combined with the <a href="/post/2026/06/02/APFS-Space-Manager">Space Manager’s</a> free queues, it ensures that block deallocation is always consistent and recoverable.</p>]]></content><author><name></name></author><category term="file-systems" /><category term="apfs" /><category term="apfs" /><category term="reaper" /><category term="garbage-collection" /><summary type="html"><![CDATA[In our post on Containers, we introduced the Reaper as the subsystem responsible for garbage collection in APFS. The Reaper handles deletions that are too large to complete within a single transaction, such as deleting an entire volume or cleaning up after a snapshot deletion. In this post, we will examine the Reaper’s on-disk structures and its multi-phase state machine.]]></summary></entry><entry><title type="html">SpiceCrypt 3.0: QSPICE Support</title><link href="https://jtsylve.blog/post/2026/06/03/spice-crypt-3.0" rel="alternate" type="text/html" title="SpiceCrypt 3.0: QSPICE Support" /><published>2026-06-03T00:00:00+00:00</published><updated>2026-06-03T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2026/06/03/spice-crypt-3.0</id><content type="html" xml:base="https://jtsylve.blog/post/2026/06/03/spice-crypt-3.0"><![CDATA[<p><a href="https://github.com/jtsylve/spice-crypt/">SpiceCrypt 3.0.0</a> is out.  When I <a href="/post/2026/03/18/PSpice-Encryption-Weakness">introduced SpiceCrypt in March</a>, it decrypted PSpice and LTspice model files so engineers could use lawfully obtained models in any simulator.  This release adds QSPICE, the protection scheme used by Qorvo’s simulator, and with it SpiceCrypt now spans the three most widely used SPICE tools in a single auto-detecting library and tool.</p>

<h2 id="whats-new">What’s new</h2>

<ul>
  <li><strong>QSPICE <code class="language-plaintext highlighter-rouge">.prot</code> decryption.</strong>  SpiceCrypt now decrypts QSPICE protected sub-circuits: randomized base-16 encoding, a seed-keyed dual stream cipher, DEFLATE decompression, and Windows-1252 detokenization.  Surrounding plaintext lines pass through untouched.  The full reverse-engineered scheme is documented in <a href="https://github.com/jtsylve/spice-crypt/blob/master/SPECIFICATIONS/qspice.md"><code class="language-plaintext highlighter-rouge">SPECIFICATIONS/qspice.md</code></a>.</li>
  <li><strong>Unified auto-detection.</strong>  <code class="language-plaintext highlighter-rouge">decrypt_stream()</code> and <code class="language-plaintext highlighter-rouge">decrypt()</code> now auto-detect across Binary File, PSpice, QSPICE, and LTspice formats.  Point SpiceCrypt at a file and it picks the right scheme.</li>
  <li><strong>New public API.</strong>  <code class="language-plaintext highlighter-rouge">QSpiceFileParser</code> and <code class="language-plaintext highlighter-rouge">QSpiceCipher</code> are now exported for callers that want to work with QSPICE directly.</li>
  <li><strong>Block-count reporting.</strong>  Since a single file can hold many protected sub-circuits, <code class="language-plaintext highlighter-rouge">decrypt_stream()</code> now returns the block count for QSPICE inputs.</li>
  <li><strong>Graceful degradation.</strong>  A protected block that fails to decode now passes through unchanged with a warning instead of aborting the whole file.</li>
</ul>

<h2 id="breaking-changes">Breaking changes</h2>

<p>The deprecated v2.0.0 backward-compatibility shims have been removed: the top-level <code class="language-plaintext highlighter-rouge">des.py</code>, <code class="language-plaintext highlighter-rouge">binary_file.py</code>, and <code class="language-plaintext highlighter-rouge">crypto_state.py</code> modules are gone.  Import the LTspice internals directly instead:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">spice_crypt.ltspice</span> <span class="kn">import</span> <span class="bp">...</span>
</code></pre></div></div>

<p>The CLI and the primary <code class="language-plaintext highlighter-rouge">decrypt</code> / <code class="language-plaintext highlighter-rouge">decrypt_stream</code> entry points are unchanged.</p>

<h2 id="upgrading">Upgrading</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install</span> <span class="nt">--upgrade</span> spice-crypt
</code></pre></div></div>

<p>Or with uv:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv tool <span class="nb">install</span> <span class="nt">--upgrade</span> spice-crypt
</code></pre></div></div>

<h2 id="links">Links</h2>

<ul>
  <li><strong>Repository</strong>: <a href="https://github.com/jtsylve/spice-crypt">github.com/jtsylve/spice-crypt</a></li>
  <li><strong>PyPI</strong>: <a href="https://pypi.org/project/spice-crypt/">pypi.org/project/spice-crypt</a></li>
  <li><strong>QSPICE specification</strong>: <a href="https://github.com/jtsylve/spice-crypt/blob/master/SPECIFICATIONS/qspice.md">SPECIFICATIONS/qspice.md</a></li>
</ul>

<p>If you run into issues or have feature requests, please <a href="https://github.com/jtsylve/spice-crypt/issues">open an issue</a> on GitHub.</p>

<p><strong>Disclaimer:</strong> SpiceCrypt is intended solely for enabling simulator interoperability with lawfully obtained models.  Using it to violate intellectual property rights is immoral and is not an acceptable use of the tool.</p>]]></content><author><name></name></author><category term="security-research" /><category term="encryption" /><category term="qspice" /><category term="spice" /><category term="reverse-engineering" /><category term="encryption" /><category term="interoperability" /><summary type="html"><![CDATA[SpiceCrypt 3.0.0 is out. When I introduced SpiceCrypt in March, it decrypted PSpice and LTspice model files so engineers could use lawfully obtained models in any simulator. This release adds QSPICE, the protection scheme used by Qorvo’s simulator, and with it SpiceCrypt now spans the three most widely used SPICE tools in a single auto-detecting library and tool.]]></summary></entry><entry><title type="html">Space Manager</title><link href="https://jtsylve.blog/post/2026/06/02/APFS-Space-Manager" rel="alternate" type="text/html" title="Space Manager" /><published>2026-06-02T00:00:00+00:00</published><updated>2026-06-15T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2026/06/02/APFS%20Space%20Manager</id><content type="html" xml:base="https://jtsylve.blog/post/2026/06/02/APFS-Space-Manager"><![CDATA[<p>In our <a href="/post/2022/12/05/APFS-Containers">earlier post on Containers</a>, 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.</p>

<h2 id="overview">Overview</h2>

<p>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 <code class="language-plaintext highlighter-rouge">nx_spaceman_oid</code> field of the <a href="/post/2022/12/06/APFS-NX-Superblock">NX Superblock</a>. The Space Manager tracks block allocation using a three-tier hierarchy: the top-level <code class="language-plaintext highlighter-rouge">spaceman_phys_t</code> structure contains per-device metadata, which references <em>Chunk Address Blocks</em> (CABs) or <em>Chunk Info Blocks</em> (CIBs) directly, which in turn reference individual allocation bitmaps.</p>

<h2 id="chunks-and-bitmaps">Chunks and Bitmaps</h2>

<p>The Space Manager divides each storage device into fixed-size <em>chunks</em>. Each chunk is a contiguous range of blocks tracked by a single allocation bitmap. The number of blocks per chunk is stored in <code class="language-plaintext highlighter-rouge">sm_blocks_per_chunk</code>.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">chunk_info</span> <span class="p">{</span>
    <span class="kt">uint64_t</span> <span class="n">ci_xid</span><span class="p">;</span>         <span class="c1">// 0x00</span>
    <span class="kt">uint64_t</span> <span class="n">ci_addr</span><span class="p">;</span>        <span class="c1">// 0x08</span>
    <span class="kt">uint32_t</span> <span class="n">ci_block_count</span><span class="p">;</span> <span class="c1">// 0x10</span>
    <span class="kt">uint32_t</span> <span class="n">ci_free_count</span><span class="p">;</span>  <span class="c1">// 0x14</span>
    <span class="n">paddr_t</span> <span class="n">ci_bitmap_addr</span><span class="p">;</span>  <span class="c1">// 0x18</span>
<span class="p">}</span> <span class="n">chunk_info_t</span><span class="p">;</span>              <span class="c1">// 0x20</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">ci_xid</code>: The transaction identifier of the last transaction that modified this chunk’s bitmap</li>
  <li><code class="language-plaintext highlighter-rouge">ci_addr</code>: The first block address of this chunk</li>
  <li><code class="language-plaintext highlighter-rouge">ci_block_count</code>: The number of blocks in this chunk. The count occupies the lower 20 bits (<code class="language-plaintext highlighter-rouge">CI_COUNT_MASK</code>, 0x000fffff); the upper 12 bits (<code class="language-plaintext highlighter-rouge">CI_COUNT_RESERVED_MASK</code>, 0xfff00000) are reserved and zero on disk.</li>
  <li><code class="language-plaintext highlighter-rouge">ci_free_count</code>: The number of free blocks in this chunk. As with <code class="language-plaintext highlighter-rouge">ci_block_count</code>, the count is the lower 20 bits (<code class="language-plaintext highlighter-rouge">CI_COUNT_MASK</code>) and the upper 12 bits (<code class="language-plaintext highlighter-rouge">CI_COUNT_RESERVED_MASK</code>) are reserved and zero on disk.</li>
  <li><code class="language-plaintext highlighter-rouge">ci_bitmap_addr</code>: The physical address of the allocation bitmap for this chunk, or zero if no bitmap has been allocated</li>
</ul>

<p>The allocator does track two per-chunk markers, one for chunks reserved to the metazone (pinned-to-main) and one for chunks currently assigned to an allocation zone, but these are runtime in-memory bookkeeping. They have no on-disk representation in <code class="language-plaintext highlighter-rouge">chunk_info_t</code> and never appear in the count fields.</p>

<h2 id="chunk-info-blocks-and-chunk-address-blocks">Chunk Info Blocks and Chunk Address Blocks</h2>

<p>Chunk info structures are grouped into <em>Chunk Info Blocks</em> (CIBs), physical objects that each hold an array of <code class="language-plaintext highlighter-rouge">chunk_info_t</code> entries.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">chunk_info_block</span> <span class="p">{</span>
    <span class="n">obj_phys_t</span> <span class="n">cib_o</span><span class="p">;</span>              <span class="c1">// 0x00</span>
    <span class="kt">uint32_t</span> <span class="n">cib_index</span><span class="p">;</span>            <span class="c1">// 0x20</span>
    <span class="kt">uint32_t</span> <span class="n">cib_chunk_info_count</span><span class="p">;</span> <span class="c1">// 0x24</span>
    <span class="n">chunk_info_t</span> <span class="n">cib_chunk_info</span><span class="p">[];</span> <span class="c1">// 0x28</span>
<span class="p">}</span> <span class="n">chunk_info_block_t</span><span class="p">;</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">cib_o</code>: The object header (type <code class="language-plaintext highlighter-rouge">OBJECT_TYPE_SPACEMAN_CIB</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">cib_index</code>: The index of this CIB within its device’s array</li>
  <li><code class="language-plaintext highlighter-rouge">cib_chunk_info_count</code>: The number of chunk info entries in this block</li>
  <li><code class="language-plaintext highlighter-rouge">cib_chunk_info</code>: A variable-length array of chunk info structures</li>
</ul>

<p>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: <em>Chunk Address Blocks</em> (CABs).</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">cib_addr_block</span> <span class="p">{</span>
    <span class="n">obj_phys_t</span> <span class="n">cab_o</span><span class="p">;</span>       <span class="c1">// 0x00</span>
    <span class="kt">uint32_t</span> <span class="n">cab_index</span><span class="p">;</span>     <span class="c1">// 0x20</span>
    <span class="kt">uint32_t</span> <span class="n">cab_cib_count</span><span class="p">;</span> <span class="c1">// 0x24</span>
    <span class="n">paddr_t</span> <span class="n">cab_cib_addr</span><span class="p">[];</span> <span class="c1">// 0x28</span>
<span class="p">}</span> <span class="n">cib_addr_block_t</span><span class="p">;</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">cab_o</code>: The object header (type <code class="language-plaintext highlighter-rouge">OBJECT_TYPE_SPACEMAN_CAB</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">cab_index</code>: The index of this CAB within its device’s array</li>
  <li><code class="language-plaintext highlighter-rouge">cab_cib_count</code>: The number of CIB addresses stored in this block</li>
  <li><code class="language-plaintext highlighter-rouge">cab_cib_addr</code>: A variable-length array of physical CIB addresses</li>
</ul>

<p>When <code class="language-plaintext highlighter-rouge">sm_cab_count</code> in the device structure is zero, CIB addresses are stored directly in the Space Manager. When nonzero, the CAB indirection layer is present.</p>

<h2 id="free-queues">Free Queues</h2>

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

<p>APFS maintains three free queues:</p>

<table style="margin-left: 0">
  <thead>
    <tr>
      <th>Name</th>
      <th>Value</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SFQ_IP</td>
      <td>0</td>
      <td>Internal pool free queue</td>
    </tr>
    <tr>
      <td>SFQ_MAIN</td>
      <td>1</td>
      <td>Main device free queue</td>
    </tr>
    <tr>
      <td>SFQ_TIER2</td>
      <td>2</td>
      <td>Tier-2 (HDD on Fusion) device free queue</td>
    </tr>
  </tbody>
</table>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">spaceman_free_queue</span> <span class="p">{</span>
    <span class="kt">uint64_t</span> <span class="n">sfq_count</span><span class="p">;</span>           <span class="c1">// 0x00</span>
    <span class="n">oid_t</span> <span class="n">sfq_tree_oid</span><span class="p">;</span>           <span class="c1">// 0x08</span>
    <span class="n">xid_t</span> <span class="n">sfq_oldest_xid</span><span class="p">;</span>         <span class="c1">// 0x10</span>
    <span class="kt">uint16_t</span> <span class="n">sfq_tree_node_limit</span><span class="p">;</span> <span class="c1">// 0x18</span>
    <span class="kt">uint16_t</span> <span class="n">sfq_pad16</span><span class="p">;</span>           <span class="c1">// 0x1A</span>
    <span class="kt">uint32_t</span> <span class="n">sfq_pad32</span><span class="p">;</span>           <span class="c1">// 0x1C</span>
    <span class="kt">uint64_t</span> <span class="n">sfq_reserved</span><span class="p">;</span>        <span class="c1">// 0x20</span>
<span class="p">}</span> <span class="n">spaceman_free_queue_t</span><span class="p">;</span>          <span class="c1">// 0x28</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">sfq_count</code>: The number of entries in this free queue</li>
  <li><code class="language-plaintext highlighter-rouge">sfq_tree_oid</code>: The object identifier of the B-Tree that stores the entries, or zero if not yet created</li>
  <li><code class="language-plaintext highlighter-rouge">sfq_oldest_xid</code>: The oldest transaction identifier among all entries</li>
  <li><code class="language-plaintext highlighter-rouge">sfq_tree_node_limit</code>: When the B-Tree node count exceeds this limit, the queue is drained more aggressively</li>
</ul>

<h3 id="free-queue-entries">Free Queue Entries</h3>

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

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">spaceman_free_queue_key</span> <span class="p">{</span>
    <span class="n">xid_t</span> <span class="n">sfqk_xid</span><span class="p">;</span>          <span class="c1">// 0x00</span>
    <span class="n">paddr_t</span> <span class="n">sfqk_paddr</span><span class="p">;</span>      <span class="c1">// 0x08</span>
<span class="p">}</span> <span class="n">spaceman_free_queue_key_t</span><span class="p">;</span> <span class="c1">// 0x10</span>
</code></pre></div></div>

<p>The value is a <code class="language-plaintext highlighter-rouge">uint64_t</code> block count. Single-block extents store a zero-length value in the B-Tree to save space (the count of 1 is implied).</p>

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

<h2 id="internal-pool">Internal Pool</h2>

<p>The <em>Internal Pool</em> (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.</p>

<p>Key fields in <code class="language-plaintext highlighter-rouge">spaceman_phys_t</code>:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">sm_ip_base</code>: The physical base address of the internal pool blocks</li>
  <li><code class="language-plaintext highlighter-rouge">sm_ip_block_count</code>: The total number of blocks in the pool (bit 63 is a fragmentation flag)</li>
  <li><code class="language-plaintext highlighter-rouge">sm_ip_bm_base</code>: The physical base address of the IP bitmap blocks</li>
  <li><code class="language-plaintext highlighter-rouge">sm_ip_bm_block_count</code>: The number of IP bitmap blocks (bit 31 is a fragmentation flag)</li>
  <li><code class="language-plaintext highlighter-rouge">sm_ip_bm_size_in_blocks</code>: The number of bitmap blocks needed to cover the pool</li>
  <li><code class="language-plaintext highlighter-rouge">sm_ip_bm_tx_multiplier</code>: The number of bitmaps per transaction (at least 4)</li>
</ul>

<p>When the fragmentation flag is set (bit 63 of <code class="language-plaintext highlighter-rouge">sm_ip_block_count</code> or bit 31 of <code class="language-plaintext highlighter-rouge">sm_ip_bm_block_count</code>), the pool blocks or bitmaps are not contiguous. Their physical addresses must be looked up through a <em>Metadata Fragmented Extent List Tree</em> rather than computed from the base address.</p>

<h2 id="allocation-zones">Allocation Zones</h2>

<p>APFS uses <em>allocation zones</em> to group related allocations together on disk, reducing fragmentation and improving sequential read performance. Each device has up to 8 allocation zones (<code class="language-plaintext highlighter-rouge">SM_DATAZONE_ALLOCZONE_COUNT</code>), with zone IDs 1 through 4 corresponding to minimum allocation sizes in blocks.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">spaceman_allocation_zone_info_phys</span> <span class="p">{</span>
    <span class="n">spaceman_allocation_zone_boundaries_t</span> <span class="n">saz_current_boundaries</span><span class="p">;</span>
    <span class="n">spaceman_allocation_zone_boundaries_t</span> <span class="n">saz_previous_boundaries</span><span class="p">[</span><span class="mi">7</span><span class="p">];</span>
    <span class="kt">uint16_t</span> <span class="n">saz_zone_id</span><span class="p">;</span>
    <span class="kt">uint16_t</span> <span class="n">saz_previous_boundary_index</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">saz_reserved</span><span class="p">;</span>
<span class="p">}</span> <span class="n">spaceman_allocation_zone_info_phys_t</span><span class="p">;</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">saz_current_boundaries</code>: The current start and end block addresses of this zone</li>
  <li><code class="language-plaintext highlighter-rouge">saz_previous_boundaries</code>: A circular buffer of the 7 most recent previous chunk assignments</li>
  <li><code class="language-plaintext highlighter-rouge">saz_zone_id</code>: The allocation size class (1-4 blocks, or 0 for unused)</li>
  <li><code class="language-plaintext highlighter-rouge">saz_previous_boundary_index</code>: Index into the circular buffer for the next rotation</li>
</ul>

<p>Each allocation zone boundary is a simple range:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">spaceman_allocation_zone_boundaries</span> <span class="p">{</span>
    <span class="kt">uint64_t</span> <span class="n">saz_zone_start</span><span class="p">;</span> <span class="c1">// 0x00</span>
    <span class="kt">uint64_t</span> <span class="n">saz_zone_end</span><span class="p">;</span>   <span class="c1">// 0x08</span>
<span class="p">}</span> <span class="n">spaceman_allocation_zone_boundaries_t</span><span class="p">;</span>
</code></pre></div></div>

<p>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. Which chunk is currently assigned to a zone is tracked by a runtime in-memory allocation-zone marker with no on-disk representation; the persistent zone state lives in <code class="language-plaintext highlighter-rouge">saz_current_boundaries</code> and <code class="language-plaintext highlighter-rouge">saz_previous_boundaries</code>.</p>

<h2 id="metazone">Metazone</h2>

<p>The <em>metazone</em> 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.</p>

<p>The metazone size scales with device capacity:</p>
<ul>
  <li>Devices smaller than approximately 6 GB have no metazone</li>
  <li>Devices smaller than 16 GB use a 512 MB metazone</li>
  <li>Larger devices use a tiered formula that allocates progressively smaller fractions as device size increases, capped at one-quarter of the total device size</li>
</ul>

<p>Metazone membership is a contiguous region derived from device geometry, not a per-chunk on-disk flag. At runtime the allocator marks chunks reserved to the metazone as pinned-to-main and excludes them from data allocation zones, but that marker is in-memory state with no on-disk representation in <code class="language-plaintext highlighter-rouge">chunk_info_t</code>.</p>

<h2 id="spaceman_phys_t">spaceman_phys_t</h2>

<p>The top-level structure tying everything together:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">spaceman_phys</span> <span class="p">{</span>
    <span class="n">obj_phys_t</span> <span class="n">sm_o</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">sm_block_size</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">sm_blocks_per_chunk</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">sm_chunks_per_cib</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">sm_cibs_per_cab</span><span class="p">;</span>
    <span class="n">spaceman_device_t</span> <span class="n">sm_dev</span><span class="p">[</span><span class="n">SD_COUNT</span><span class="p">];</span>
    <span class="kt">uint32_t</span> <span class="n">sm_flags</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">sm_ip_bm_tx_multiplier</span><span class="p">;</span>
    <span class="kt">uint64_t</span> <span class="n">sm_ip_block_count</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">sm_ip_bm_size_in_blocks</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">sm_ip_bm_block_count</span><span class="p">;</span>
    <span class="n">paddr_t</span> <span class="n">sm_ip_bm_base</span><span class="p">;</span>
    <span class="n">paddr_t</span> <span class="n">sm_ip_base</span><span class="p">;</span>
    <span class="kt">uint64_t</span> <span class="n">sm_fs_reserve_block_count</span><span class="p">;</span>
    <span class="kt">uint64_t</span> <span class="n">sm_fs_reserve_alloc_count</span><span class="p">;</span>
    <span class="n">spaceman_free_queue_t</span> <span class="n">sm_fq</span><span class="p">[</span><span class="n">SFQ_COUNT</span><span class="p">];</span>
    <span class="kt">uint16_t</span> <span class="n">sm_ip_bm_free_head</span><span class="p">;</span>
    <span class="kt">uint16_t</span> <span class="n">sm_ip_bm_free_tail</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">sm_ip_bm_xid_offset</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">sm_ip_bitmap_offset</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">sm_ip_bm_free_next_offset</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">sm_version</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">sm_struct_size</span><span class="p">;</span>
    <span class="n">spaceman_datazone_info_phys_t</span> <span class="n">sm_datazone</span><span class="p">;</span>
    <span class="c1">// Variable-length arrays follow...</span>
<span class="p">}</span> <span class="n">spaceman_phys_t</span><span class="p">;</span>
</code></pre></div></div>

<p>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.</p>

<p>Each device is described by a <code class="language-plaintext highlighter-rouge">spaceman_device_t</code>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">spaceman_device</span> <span class="p">{</span>
    <span class="kt">uint64_t</span> <span class="n">sm_block_count</span><span class="p">;</span>  <span class="c1">// 0x00</span>
    <span class="kt">uint64_t</span> <span class="n">sm_chunk_count</span><span class="p">;</span>  <span class="c1">// 0x08</span>
    <span class="kt">uint32_t</span> <span class="n">sm_cib_count</span><span class="p">;</span>    <span class="c1">// 0x10</span>
    <span class="kt">uint32_t</span> <span class="n">sm_cab_count</span><span class="p">;</span>    <span class="c1">// 0x14</span>
    <span class="kt">uint64_t</span> <span class="n">sm_free_count</span><span class="p">;</span>   <span class="c1">// 0x18</span>
    <span class="kt">uint32_t</span> <span class="n">sm_addr_offset</span><span class="p">;</span>  <span class="c1">// 0x20</span>
    <span class="kt">uint32_t</span> <span class="n">sm_reserved</span><span class="p">;</span>     <span class="c1">// 0x24</span>
    <span class="kt">uint64_t</span> <span class="n">sm_reserved2</span><span class="p">;</span>    <span class="c1">// 0x28</span>
<span class="p">}</span> <span class="n">spaceman_device_t</span><span class="p">;</span>          <span class="c1">// 0x30</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">sm_block_count</code>: Total blocks on this device</li>
  <li><code class="language-plaintext highlighter-rouge">sm_chunk_count</code>: Number of chunks</li>
  <li><code class="language-plaintext highlighter-rouge">sm_cib_count</code>: Number of CIBs</li>
  <li><code class="language-plaintext highlighter-rouge">sm_cab_count</code>: Number of CABs (zero if CIBs are stored directly)</li>
  <li><code class="language-plaintext highlighter-rouge">sm_free_count</code>: Total free blocks on this device</li>
  <li><code class="language-plaintext highlighter-rouge">sm_addr_offset</code>: Byte offset within <code class="language-plaintext highlighter-rouge">spaceman_phys_t</code> where the CIB/CAB address array begins</li>
</ul>

<h2 id="forensic-considerations">Forensic Considerations</h2>

<p>The Space Manager is particularly valuable for forensic analysis:</p>

<ul>
  <li><strong>Free queue entries</strong> identify blocks that were recently freed but may still contain recoverable data. The transaction identifier on each entry indicates when the block was freed.</li>
  <li><strong>Allocation bitmaps</strong> reveal which blocks are currently in use versus free, which can be cross-referenced against file extent records to find orphaned data.</li>
  <li><strong>Chunk info transaction identifiers</strong> (<code class="language-plaintext highlighter-rouge">ci_xid</code>) indicate when each chunk’s allocation state last changed, providing a coarse timeline of write activity across the disk.</li>
  <li><strong>Allocation zones</strong> reveal where the file system tends to place related data, which can help reconstruct file system activity patterns.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>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.</p>]]></content><author><name></name></author><category term="file-systems" /><category term="apfs" /><category term="apfs" /><category term="space-manager" /><category term="allocation" /><summary type="html"><![CDATA[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.]]></summary></entry></feed>