Apple File System (APFS) Firmlink Black Magic

This article is also available in: 中文

Apple introduced a new way of file system hierarchy that can separate the system and user data within an APFS system container since macOS Catalina: Macintosh HD, a volume which is mounted as read-only for system files; and Macintosh HD - Data, a volume which is mounted as writable and for all user intractable files such as applications and preferences. There are also other volumes such as Recovery that is used for RecoveryOS:

$ diskutil list
/dev/disk0 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *1.0 TB     disk0
   1:             Apple_APFS_ISC Container disk2         524.3 MB   disk0s1
   2:                 Apple_APFS Container disk3         994.7 GB   disk0s2
   3:        Apple_APFS_Recovery Container disk1         5.4 GB     disk0s3

/dev/disk3 (synthesized):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      APFS Container Scheme -                      +994.7 GB   disk3
                                 Physical Store disk0s2
   1:                APFS Volume Macintosh HD - Data     410.4 GB   disk3s1
   2:                APFS Volume Macintosh HD            8.9 GB     disk3s3
   3:              APFS Snapshot 8.9 GB     disk3s3s1
   4:                APFS Volume Preboot                 4.7 GB     disk3s4
   5:                APFS Volume Recovery                769.9 MB   disk3s5
   6:                APFS Volume VM                      2.1 GB     disk3s6

The role of each volume was assigned in its superblock structure (apfs_superblock_t), which can be found in Apple’s APFS Reference:

#define APFS_VOL_ROLE_NONE      0x0000
#define APFS_VOL_ROLE_SYSTEM    0x0001
#define APFS_VOL_ROLE_USER      0x0002
#define APFS_VOL_ROLE_RECOVERY  0x0004
#define APFS_VOL_ROLE_VM        0x0008
#define APFS_VOL_ROLE_PREBOOT   0x0010
#define APFS_VOL_ROLE_DATA      0x0040
#define APFS_VOL_ROLE_BASEBAND  0x0080

Although this is new to macOS at the time, the scheme was already used in iOS for a while. When the system booted, both Macintosh HD (APFS_VOL_ROLE_SYSTEM) and Macintosh HD - Data (APFS_VOL_ROLE_DATA) will be presented as a single logical volume with both their contents combined with the magic of firmlinks.

Firmlinks are similar to symbolic links but allows bi-directional travel and only for directories. This effectively allow macOS to merge the contents of Macintosh HD and Macintosh HD - Data, e.g. /Users on the read-only Macintosh HD is essentially a firmlink of /Shared on the writable Macintosh HD - Data. This would also make Apple’s System Integrity Protection (SIP) more seamless, e.g. SIP-protected system applications resides in read-only /System/Applications and user applications can be installed in the /Applications, while the user see the combination of both in the root /Applications.

The system firmlinks can be found at /usr/share/firmlinks:

$ cat /usr/share/firmlinks
/AppleInternal	AppleInternal
/Applications	Applications
/Library	Library
/System/Library/Caches	System/Library/Caches
/System/Library/Assets	System/Library/Assets
/System/Library/PreinstalledAssets	System/Library/PreinstalledAssets
/System/Library/AssetsV2	System/Library/AssetsV2
/System/Library/PreinstalledAssetsV2	System/Library/PreinstalledAssetsV2
/System/Library/CoreServices/CoreTypes.bundle/Contents/Library	System/Library/CoreServices/CoreTypes.bundle/Contents/Library
/System/Library/Speech	System/Library/Speech
/Users	Users
/Volumes	Volumes
/cores	cores
/opt	opt
/private	private
/usr/local	usr/local
/usr/libexec/cups	usr/libexec/cups
/usr/share/snmp	usr/share/snmp

To create root-level directories since Catalina, we need to rely on synthetic firmlinks:

  • Create a file in /etc named synthetic.conf.
  • Make sure its permissions were set to:
root: read, write
wheel: read
everyone: read

(Usually command “sudo touch /etc/synthetic.conf” would do all the above.)

In order to link directory /Users/foo/bar to /baz, we can add a new entry to the file we just created as following:

# create a firmlink named "baz" at / which points to "Users/foo/bar"
baz Users/foo/bar

You can also check man synthetic.conf for all available options. Note that the two should be separate with a tab instead of spaces or it won’t work. Also the paths are not started with /. Now reboot the system and see the changes we just made.