APFS Firmlinks and the Split Volume Design
Background: why the split exists
Starting with macOS Catalina (10.15), Apple split the OS into two APFS volumes:
- System volume (Macintosh HD): read-only, contains the OS itself
- Data volume (Macintosh HD - Data): read-write, contains everything mutable
The motivation was the APFS - Sealed System Volume (SSV): Apple wanted to cryptographically verify the integrity of every OS file at boot. You can only do that with a read-only volume. But users and apps need /Applications, /Users, /Library, /var etc. to be writable. The solution was to put the OS on one volume and all mutable content on another, then stitch them together so it looks like one filesystem.
What a firmlink is
A firmlink is a kernel-level path redirection, implemented inside the APFS VFS layer. It is stored as a special inode type on the System volume. When the kernel resolves a path that hits a firmlink inode, it transparently redirects the traversal to the corresponding path on the Data volume.
It is not a symlink:
- A symlink is a file containing a path string; any process resolving it sees a redirect at the
readlinklevel - A firmlink has no path string visible to userspace; the redirection happens inside
vfs_lookupbefore it ever returns to userspace
It is not a bind mount:
- A bind mount creates a new
vfsmountentry; tools likemountcan see it;st_devchanges at the mount point - A firmlink does not create a new mount point;
mountdoesn’t see it;st_devdoes not change when you cross a firmlink (this is the critical fact that breaksdu)
The firmlink table is stored at /usr/share/firmlinks on the System volume:
/Applications Applications
/Users Users
/Library Library
/System/Library/Staged Extensions System/Library/Staged Extensions
...
Left side = path on the System volume. Right side = path on the Data volume (relative to its root).
Why du double-counts across firmlinks
du detects filesystem boundaries by comparing st_dev (device number) of inodes. When it crosses a mount point, st_dev changes, and du -x stops there.
Firmlinks do not change st_dev. When du walks /Users from the System volume, it is actually reading the Data volume’s /Users directory — but st_dev stays the same as the System volume’s device number, because no mount point was crossed. So du -x / will walk into Data volume content as if it were on the System volume.
Then if you also du -x /System/Volumes/Data, you walk the same /Users directory again, this time from the Data volume’s root. You’ve counted it twice.
Why du fails at the Data volume root
Firmlinks are bidirectional: /Users on the root points into the Data volume, and /System/Volumes/Data/Users points back. When du encounters a firmlink target at /System/Volumes/Data/, it treats it as a boundary and refuses to descend. This is not a permissions issue — it’s structural.
# This returns almost nothing — firmlinks block traversal
sudo du -hd1 /System/Volumes/Data/
# This works — you enter the firmlink target directly
sudo du -sh /System/Volumes/Data/Users/
sudo du -sh /System/Volumes/Data/Library/To see which directories are firmlinked:
cat /usr/share/firmlinksCorrect approach for disk accounting
Enumerate each top-level directory on the Data volume individually:
sudo du -sh /System/Volumes/Data/Applications/ \
/System/Volumes/Data/Library/ \
/System/Volumes/Data/Users/ \
/System/Volumes/Data/private/ \
/System/Volumes/Data/opt/ \
/System/Volumes/Data/usr/ \
/System/Volumes/Data/System/ \
/System/Volumes/Data/.DocumentRevisions-V100/ \
/System/Volumes/Data/.Spotlight-V100/ \
/System/Volumes/Data/.fseventsd/ \
/System/Volumes/Data/cores/ 2>/dev/nullCommands that do NOT work
du -hd1 /System/Volumes/Data/— firmlinks block traversal at rootdu -hxd2 /System/Volumes/Data/—-xdoes not help with firmlinks- Any
duthat starts at the Data volume root and expects to recurse
The correct mental model
Think of the System volume and Data volume as two overlay layers, with the System volume providing the immutable OS skeleton and the Data volume providing the mutable flesh. When you’re at a terminal, / is the stitched-together view. But for disk accounting purposes, they are separate volumes.
Even after correctly enumerating all directories, the total from du may be far less than what diskutil apfs list reports. The gap can be deleted-but-open files (processes holding file descriptors to deleted files), snapshot-retained blocks, or APFS metadata.
See also
- APFS - What It Is — overview of APFS design
- APFS - Sealed System Volume (SSV) — the security model that motivated the split
- macOS Disk Reporting — firmlinks are one of four
dufailure modes - Deleted-But-Open Files — the gap between
dutotals anddiskutilis often deleted-open files