Dissecting Apple's Sparse Image Format (ASIF)
Analyzing Apple's Sparse Image Format (ASIF)
Originally published by Erik Schamper on schamper.dev Date: 2026-06-18 | Reading Time: minutes
During WWDC 2025, Apple unveiled macOS 26 Tahoe, which introduced a novel disk image format known as ASIF.
Integrated primarily within the Virtualization framework, ASIF draws heavy influence from established virtual disk standards. In essence, it is a sparse virtual disk format, operating similarly to QCOW2, VHDX, or sparse VMDK files. For those unfamiliar, "sparse" means the file can represent a massive virtual disk while only consuming physical space for the data actually written to it.
Shortly before Tahoe's official launch in late 2025, I decided to challenge myself by building a parser for ASIF. While some time has passed, I want to document my methodology for tackling these problems, as it may provide a roadmap for those new to the art of reverse engineering file formats.
Throughout this guide, I have included specific Research Notes to provide deeper context.
🛠️ Phase 1: Environment Setup & Test Data
To begin, we need a controlled sample. I'll use the official Apple documentation to generate a file, then populate it with a predictable pattern.
Research Note: When testing, I prefer writing a pattern where the content is tied to the offset. While there are more sophisticated patterns, simply filling the file with verifiable data makes the subsequent analysis much smoother.
Step-by-Step Execution
- Create a 1GiB blank ASIF image.
- Attach the image to a device node.
- Write a sequential byte pattern using Python.
- Detach the image.
# Create the sparse image
❯ diskutil image create blank --fs none --format ASIF --size 1GiB file
file.asif created
# Attach the image without mounting the filesystem
❯ diskutil image attach -nomount file.asif /dev/disk4
Now, I'll use a Python script to write 1MB chunks of incrementing bytes (0-255) to the disk:
# Python 3.14.0 (main, Oct 7 2025) on darwin
fh = open("/dev/disk4", "wb")
for i in range(255):
# Write 1MB of the current byte value
fh.write(bytes([i] * 1024 * 1024))
fh.close()
# Clean up
❯ hdiutil detach disk4
"disk4" ejected.
🔍 Phase 2: Initial Heuristic Analysis
The first step in any reverse engineering task is "eyeballing" the hexdump to find patterns.
❯ xxd file.asif | head -5
0000 0000: 73 68 64 77 00 00 00 01 00 00 02 00 00 00 00 00 shdw....
0000 0010: 00 00 00 00 00 00 02 00 00 00 00 00 00 04 14 00 ........
0000 0020: 8a f9 ea d2 cf 38 49 c0 8e ec 00 95 cf 5c 78 99 ....8I...\x
0000 0030: 00 00 00 00 00 1d cd 65 00 00 08 00 00 00 00 00 .....e....
0000 0040: 00 10 00 00 02 00 00 00 00 00 00 00 ff ff ff ff ........
Immediately, we see a "magic" string (shdw) followed by what appear to be big-endian integers.
Research Note:
- Magic Bytes: Always search for these strings/hex values on Google, GitHub, or VirusTotal Retrohunt to see if the format is already documented.
- Endianness: Identifying integer widths is like riding a bike. Scan in 4-byte (
uint32) or 8-byte (uint64) chunks. Look for "round" hex numbers or values that correlate with file offsets or known sizes.
The Reverse Engineering Workflow
📐 Phase 3: Defining the Structure
I'll use dissect.cstruct to create a preliminary hypothesis of the header layout.
# /// script
# requires-python = ">=3.10"
# dependencies = ["dissect.cstruct"]
# ///
import sys
from dissect.cstruct import cstruct, dumpstruct
asif_def = """
struct header {
char magic[4];
uint32 field4;
uint32 field8;
uint32 fieldC;
uint64 field10;
uint64 field18;
char field20[16];
uint64 field30;
uint64 field38;
uint32 field40;
uint32 field44;
uint32 field48;
uint32 field4C;
};
"""
c_asif = cstruct(asif_def, endian=">") # Big Endian
with open(sys.argv[1], "rb") as fh:
header = c_asif.header(fh)
dumpstruct(header)
Header Analysis Results
| Field | Offset | Size | Value | Interpretation |
|---|---|---|---|---|
magic[4] | 0x0000 | 4 bytes | b'shdw' | File Signature |
field4 | 0x0004 | 4 bytes | 0x1 | Unknown |
field8 | 0x0008 | 4 bytes | 0x200 | Sector Size (512B) |
fieldC | 0x000c | 4 bytes | 0x0 | Unknown |
field10 | 0x0010 | 8 bytes | 0x200 | Possible Offset |
field18 | 0x0018 | 8 bytes | 0x41400 | Possible Offset |
field20[16] | 0x0020 | 16 bytes | \x8a\xf9... | Random/Hash? |
field30 | 0x0030 | 8 bytes | 0x1dcd65 | |
field38 | 0x0038 | 8 bytes | 0x80000000000 | Unknown |
field40 | 0x0040 | 4 bytes | 0x100000 | Unknown |
field44 | 0x0044 | 4 bytes | 0x2000000 | Unknown |
field48 | 0x0048 | 4 bytes | 0x0 | Unknown |
field4C | 0x004c | 4 bytes | 0xffffffff | End Marker? |
Key Observations
We see 0x200 appearing twice, which strongly suggests a sector size of 512 bytes.
Regarding the total size, we can perform a simple calculation:
While field30 in my specific dump showed 0x1dcd65, the author notes that this field likely represents the virtual disk size in sectors. If field8 is indeed the sector size, then field10 and field18 are likely pointers to other sections of the file.
To further verify these assumptions, I'll expand the hexdump analysis:
❯ xxd -a file.asif | head -32
(Analysis continues...)