Skip to content
HN On Hacker News ↗

Dissecting Apple's Sparse Image Format (ASIF) | schamper.dev

▲ 162 points 24 comments by supermatou 1w ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is fully human-written

1 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 10 of 10
SEGMENTS · AI 0 of 10
WORD COUNT 1,374
PEAK AI % 2% · §9
Analyzed
Jun 29
backend: pangram/v3.3
Segments scanned
10 windows
avg 137 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,374 words · 10 segments analyzed

Human AI-generated
§1 Human · 0%

2026-06-18 · 18 min read · Erik Schamper At WWDC 2025, Apple announced macOS 26 Tahoe. One of the new features in macOS Tahoe is a new disk image format: ASIF. Designed for use with virtual machines (its documentation lives under the Virtualization framework), ASIF takes a lot of inspiration from existing virtual disk formats. Practically, that means it’s another sparse virtual disk format, and functions very similar to sparse VMDK, VHDX or QCOW2 files (for the uninitiated, it allow you to store a large disk, or file, in a smaller, “sparse” manner). Shortly before the release of macOS Tahoe (late 2025), I thought it’d be a fun exercise to try and write a parser for ASIF files. It’s been a while since then, but I wanted to go back and show my process on how I approach these kinds of problems. Maybe someone unfamiliar with reverse engineering file formats can pick up a thing or two. For that reason, you can find the occasional “Research note” sprinkled throughout this post with some additional insights. Let’s create a test file with the command listed in the Apple documentation, write a test pattern to it and get started:

Research note For testing purposes, I usually like to write a test pattern that allows me to verify the content matches the “offset”. In this case, basically just numbered 1 MiB blocks of bytes. There are definitely better test patterns, but for an initial peek at the file format, it’s also important to just fill up the file with anything. Having a predictable and verifiable pattern can make later steps easier.

§2 Human · 0%

❯ diskutil image create blank --fs none --format ASIF --size 1GiB file

file.asif created

❯ diskutil image attach -nomount file.asif /dev/disk4

❯ python3 Python 3.14.0 (main, Oct 7 2025, 09:34:52) [Clang 17.0.0 (clang-1700.3.19.1)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> fh = open("/dev/disk4", "wb") >>> for i in range(255): ... fh.write(bytes([i] * 1024 * 1024)) >>> fh.close()

❯ hdiutil detach disk4 "disk4" ejected.

Eyeball hexdumps As usual, we start by eyeballing some hexdumps to see if we can discern some details. ❯ xxd file.asif | head -5

0000000073686477000000010000020000000000

shdw············

0000001000000000000002000000000000041400

················

000000208af9ead2cf3849c08eec0095cf5c7899

·····8I······\x·

§3 Human · 0%

0000003000000000001dcd650000080000000000

·······e········

00000040001000000200000000000000ffffffff

················

We can immediately spot some kind of file magic, followed by some big endian looking integers.

Research note Whenever you’re reverse engineering a file format and you see some magic bytes, it’s always a good idea to search for any available information on it online. I usually search for a combination of the string/byte representation, big endian hex and little endian hex of the file magic in various search engines (Google, GitHub, VirusTotal Retrohunt). In this case, I didn’t find much useful information. As for “spotting endianness” or integer fields, it’s almost like riding a bicycle after a while. I guess a tip is to scan from left to right in chunks of 4 bytes (uint32), then 8 bytes (uint64), then possibly dividing into smaller chunks (uint16 or even uint8), until you can parse out reasonable looking integers (round base 16, or cross reference with offsets in the file, optionally multiplied by other values you spot). If you see “natural order” looking integers, it’s big endian. If it looks reverse, it’s little endian.

Let’s quickly type up a rough structure, making a best guess at the integer widths and inspect it further with dissect.cstruct: # /// script # requires-python = ">=3.10" # dependencies = ["dissect.cstruct"] # ///

import sys

from dissect.cstruct import cstruct, dumpstruct

asif_def

§4 Human · 1%

= """ 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=">")

with open(sys.argv[1], "rb") as fh: header = c_asif.header(fh) dumpstruct(header)

0000000073686477000000010000020000000000

shdw············

0000001000000000000002000000000000041400

················

000000208af9ead2cf3849c08eec0095cf5c7899

·····8I······\x·

0000003000000000001dcd650000080000000000

·······e········

00000040001000000200000000000000ffffffff

················

header magic[4] 0x0000 4

§5 Human · 1%

bytes b'shdw' field4 0x0004 4 bytes 0x1 field8 0x0008 4 bytes 0x200 fieldC 0x000c 4 bytes 0x0 field10 0x0010 8 bytes 0x200 field18 0x0018 8 bytes 0x41400 field20[16] 0x0020 16 bytes b'\x8a\xf9\xea\xd2\xcf8I\xc0\x8e\xec\x00\x95\xcf\\x\x99' field30 0x0030 8 bytes 0x1dcd65 field38 0x0038 8 bytes 0x80000000000 field40 0x0040 4 bytes 0x100000 field44 0x0044 4 bytes 0x2000000 field48 0x0048 4 bytes 0x0 field4C 0x004c 4 bytes 0xffffffff

Some interesting numbers, we can immediately spot two instances of 0x200, a common sector size (512 bytes). After playing around a bit multiplying and dividing different values, we can also figure out that field30 might be the virtual disk size in sector numbers (1GiB // 512 = 0x200000).

§6 Human · 1%

If we assume for now that field8 is the sector size, field10 and field18 might be offsets into the file. Let’s look at a slightly larger hexdump: ❯ xxd -a file.asif | head -32

0000000073686477000000010000020000000000

shdw············

0000001000000000000002000000000000041400

················

000000208af9ead2cf3849c08eec0095cf5c7899

·····8I······\x·

0000003000000000001dcd650000080000000000

·······e········

00000040001000000200000000000000ffffffff

················

0000005000000000000000000000000000000000

················

*

0000020000000000000000020000000000000004

················

§7 Human · 2%

0000021000000000000000000000000000000000

················

*

0004124000000000000000000000000000000001

················

0004125000000000000000000000000000000000

················

*

0004140000000000000000010000000000000000

················

0004141000000000000000000000000000000000

················

*

0008244000000000000000000000000000000001

················

0008245000000000000000000000000000000000

················

*

00120030c0000000000000020000000000000003

§8 Human · 2%

················

0012004000000000000000000000000000000000

················

*

002000006d657461000000010000020000000000

meta············

0020001000000200000000000000000000000000

················

0020002000000000000000000000000000000000

················

*

002002003c3f786d6c2076657273696f6e3d2231

<?xml version="1

002002102e302220656e636f64696e673d225554

.0" encoding="UT

00200220462d38223f3e0a3c21444f4354595045

F-8"?>·<!DOCTYPE

0020023020706c697374205055424c494320222d

§9 Human · 2%

plist PUBLIC "-

002002402f2f4170706c652f2f44544420504c49

//Apple//DTD PLI

00200250535420312e302f2f454e222022687474

ST 1.0//EN" "htt

Much more revealing. We can spot some data at 0x00000200 and 0x00041400, although we can’t quite make sense of it yet. At 0x00200000 we can also see meta, followed by an XML plist at 0x00200200. Maybe we were wrong about field30 being the virtual disk size? There are a little too many variables and not enough clear structure to continue with just eyeballing hexdumps alone, so let’s look for a binary to reverse. Looking for our binary There are a few ways we can go about looking for our binary, but since I wasn’t in any particular hurry I took a pretty lazy approach:

❯ grep -r "ASIF" /System/Library/Frameworks /System/Library/PrivateFrameworks [...] Binary file /System/Library/PrivateFrameworks/DiskImages2.framework/Versions/A/XPCServices/diskimagescontroller.xpc/Contents/MacOS/diskimagescontroller matches [...]

Research note I often use YARA for this purpose instead of grep, which is usually a bit faster, but this will do for now. Another approach I sometimes take is to do a needle search on the raw disk (usually the virtual disk of a VM), and then do a reverse lookup to see which file corresponds to the offsets on disk that I get a match on. Totally overkill for this, but a useful method for other cases.

§10 Human · 0%

Out of our matches, diskimagescontroller jumps out the most, and running strings on it reveals a lot of ASIF related content, including very valuable log messages, mangled function signatures and type names!

❯ strings diskimagescontroller | grep -i "asif" [...] N7di_asif7details3dirE N7di_asif7details8dir_baseE [...] Invalid value for asif header field: %s Size cannot exceed max ASIF size Unexpected ASIF header length ( [...] asif_header Couldn't read asif's header [...]

Let’s throw this puppy into IDA!

The first stop of reverse engineering anything is to look for references to interesting looking strings, and this time is no exception. For a file format, it makes sense to first focus on how to interpret the file’s header, and then go from there. We already saw some interesting strings related to “asif’s header” with the strings command, so let’s just look at some of the functions that reference those strings.

Research note At the time I did this research, I went searching online for some of the symbol names I encountered. From this I learned that there are projects on GitHub were people diff iOS firmware updates, but also that the iOS version of the same binary might contain some more symbols/strings. I ended up reverse engineering the iOS version of this binary, but not necessarily for this reason. If there are both x86 and ARM variants of the same binary, I like to have them open simultaneously. The decompiler output of one version can sometimes be much clearer than the other. I have no real data to back this up, but my gut feeling is because of the varying levels of compiler, but also decompiler optimizations for both architectures. Regardless, I remember the version of IDA I was using would crash randomly on the x86 variant of the binary, so I just spent most of my time annotating the iOS version, even though I don’t think there were actually any meaningful differences between the two. Anyway, I’m rambling, let’s continue!