Apple Defences - APFS and the SSV
Skip the usual preamble: most of mine add no value anyway. This one’s different though.
Not the usual “It took me time to write…” - although it’s true. I took some days off, and days off imply no laptop.
Second - one of my previous posts (Filesystem Wars: Why Your Choice of Storage is Actually a Security Move) has been heavily criticised by trolls on Reddit. Therefore the linuxFanboysSuck or TrollTurd prompts. They wanted some visibility, here they go! Who am I to deny them the pleasure to embarrass themselves?
Third - the elephant in the room: I am not a native speaker. My English is what it is, and yes, I do use LLMs. Mainly to polish my English, and to soften my bluntness. The damn content is mine! If you want to understand the “LLM policy”, please have a read: Memento, deinde loquere To keep it short: LLMs are what saves people from sentences like “if you don’t like it, you can s** my mot* d** you a** * * ** *** *** **** ***** ….” (hope you recognised the Fibonacci sequence. If not, try the Fibonacci Soup. It’s the soup of yesterday mixed with the soup of the day before).
Introduction
So, what is this post about? Originally, I wanted to prepare something that showed how Apple uses APFS to build a strong defence for macOS. Then while writing the raw contents I realised that there’s a deeper strategy. I will try to describe it, but this is a broad topic, so I expect follow-up notes, corrections, and possibly a few errata corrigenda. TL;DR: if you find this article interesting, you may want to come back. So, we will see the filesystem and some other security controls interacting as a great example of Defence in depth.
I would also ask the readers to highlight aspects I forgot, and if you can link these concepts to others (even in other OSes), please feel free to get in touch with me. I am @gbiondo on the Mastodon instance infosec.exchange. Kudos, mentions, and toasts will be reciprocated!
How it all begins
You’re on Facebook/Reddit/OnlyFans/Instagram/../dirTransv/../Tinder/X and you see the usual newbie post:
I just installed Kali Linux on my machine.
Now what?
Chances are that 97% of the answers are like
remove the french language support with
sudo rm -rf / --no-preserve-root
A touching example of community spirit, but the joke is so worn out by now that if you still type it, please stop reading me.
That command is meant to nuke the system. On modern Linux boxes, plain rm -rf / should be stopped by the default --preserve-root failsafe, but --no-preserve-root is literally there to bypass it. I haven’t bothered testing it on Linux for this post: the macOS side is what we care about, and the contrast with Linux is enough as a thought experiment.
But before applying the same prank to a macOS box — and seeing why it doesn’t quite work the same way — let’s look at how Apple has organised the filesystem.
Notice the philosophy: Unix tells you ‘I think you’re about to do something stupid, but I’ll let you’. macOS, as we’ll see, is going to tell you ‘you’re being stupid, and I won’t let you’ — which is either a feature or a bug depending on which side of the kernel you sit on.
The filesystem
Let’s take a quick step back to the structure of the filesystem. The APFS structure and operating philosophy have been already described in the aforementioned post, but just to refresh the ideas:
- a disk is subdivided into partitions
- in the normal case, a partition hosts an APFS container
- the container hosts one or more APFS volumes
- the free space inside the container is shared among those volumes, which can grow or shrink independently
This taxonomy is evident when using diskutil. From the man page:
**DESCRIPTION**
**diskutil** manipulates the structure of local disks. It provides
information about, and allows the administration of, partitioning schemes,
layouts, and formats of disks. This includes hard disks, solid state disks,
optical discs, disk images, APFS volumes, CoreStorage volumes, and
AppleRAID sets. It generally manipulates whole volumes instead of
individual files and directories.
Although the man page is quite useful, sometimes you just want the quick reference. Run diskutil without arguments and it will dump its help.
Let’s launch diskutil list to obtain:
gabriel@virtosaurus-erectus ~ % diskutil list
/dev/disk0 (internal, physical):
#: TYPE NAME SIZE IDENTIFIER
0: GUID_partition_scheme *68.7 GB disk0
1: Apple_APFS_ISC Container disk2 524.3 MB disk0s1
2: Apple_APFS Container disk4 62.8 GB disk0s2
3: Apple_APFS_Recovery Container disk3 5.4 GB disk0s3
/dev/disk1 (internal, physical):
#: TYPE NAME SIZE IDENTIFIER
0: FDisk_partition_scheme *13.6 MB disk1
1: Apple_HFS Guest 13.6 MB disk1s1
/dev/disk4 (synthesized):
#: TYPE NAME SIZE IDENTIFIER
0: APFS Container Scheme - +62.8 GB disk4
Physical Store disk0s2
1: APFS Volume Macintosh HD 12.6 GB disk4s1
2: APFS Snapshot com.apple.os.update-... 12.6 GB disk4s1s1
3: APFS Volume Preboot 7.2 GB disk4s2
4: APFS Volume Recovery 1.3 GB disk4s3
5: APFS Volume Data 2.8 GB disk4s5
6: APFS Volume VM 20.5 KB disk4s6
This output is per se a small pedagogical masterpiece that shows the organisation/layering of APFS.
First: the output tracks three top-level disk devices, but one is synthesized. The (further!) abstraction layer introduced by APFS is made explicit by diskutil.
disk0 is the physical disk using a GUID Partition Table (yep - GPT. The tradition of acronyms is reaching its theoretical collision limits). It has three partitions:
Apple_APFS_ISC: the iBoot System Container (ISC). Contains system-critical boot material used by iBoot on Apple Silicon machines.Apple_APFS_Recovery- the recovery container, hosting an independent recoveryOS.Apple_APFS- the “real” APFS partition.
disk4 is the “materialisation” of disk0s2. In detail:
| Disk | Label | Purpose |
|---|---|---|
disk4s1 |
Macintosh HD |
System volume |
disk4s1s1 |
com.apple.os.update-... |
Snapshot of disk4s1 |
disk4s2 |
Preboot |
Kernel cache, boot policies, and boot support material |
disk4s3 |
Recovery |
recoveryOS. Local. |
disk4s5 |
Data |
Writable volume - more on this later |
disk4s6 |
VM |
Swap/Sleepimage. Isolated to ease wiping. |
Why this organisation - it looks redundant and cumbersome! Hah, you puny penguin preacher, you don’t see the structure which I hide on purpose :). Look at the disk4s1 + disk4s1s1 combo: volume + snapshot. The system boots from the sealed snapshot, not from the writable volume. The snapshot is cryptographically sealed, and changing this would imply breaking a Merkle-tree-like chain of hashes that reaches up to Secure Boot. Therefore, attackers with root privileges can’t persistently write in /usr/bin because they’re not writing there at all. They’re writing to a volume that nobody reads at the next boot.
But actually this mechanism - cryptographically sealed volumes, which we are only glancing at now - probably grants one of the strongest forms of system integrity. Yeah, root cannot do everything (well… not really. But there are caveats.), but - do you really think that having a user who can do anything on a machine improves its security? Take your time, think about it…
Lately I think that root (both as user and privileges) is the toy of late-90s/beginning-2000s sysadmins. Great thing, sure… but the world has changed. Already the introduction of doas instead of su is a strong indicator. Personal opinion. But this blog is opinionated.
Snapshots
Now, look at this simple output:
gabriel@virtosaurus-erectus ~ % diskutil apfs list
APFS Containers (3 found)
|
+-- Container disk4 5531F5C3-04B2-45E4-84FE-72296C71CD67
====================================================
APFS Container Reference: disk4
Size (Capacity Ceiling): 62826479616 B (62.8 GB)
Capacity In Use By Volumes: 25124798464 B (25.1 GB) (40.0% used)
Capacity Not Allocated: 37701681152 B (37.7 GB) (60.0% free)
|
+-< Physical Store disk0s2 0488C2FD-8C78-46F6-BB6A-D4215930C0EB
| -----------------------------------------------------------
| APFS Physical Store Disk: disk0s2
| Size: 62826479616 B (62.8 GB)
|
+-> Volume disk4s1 C806F908-958F-4945-ADB7-29B5646526E9
| ---------------------------------------------------
| APFS Volume Disk (Role): disk4s1 (System)
| Name: Macintosh HD (Case-insensitive)
| Mount Point: Not Mounted
| Capacity Consumed: 12551831552 B (12.6 GB)
| Sealed: Yes
| FileVault: No
| |
| Snapshot: 7CF13368-3CF2-480F-8518-6626E86ACCFE
| Snapshot Disk: disk4s1s1
| Snapshot Mount Point: /
| Snapshot Sealed: Yes
|
+-> Volume disk4s2 15FD3EDD-17B5-4855-8D8A-C76AA5EC5D18
| ---------------------------------------------------
| APFS Volume Disk (Role): disk4s2 (Preboot)
| Name: Preboot (Case-insensitive)
| Mount Point: /System/Volumes/Preboot
| Capacity Consumed: 7238791168 B (7.2 GB)
| Sealed: No
| FileVault: No
|
+-> Volume disk4s3 AD1D82CF-E831-48FE-9A59-06E74FFEAA27
| ---------------------------------------------------
| APFS Volume Disk (Role): disk4s3 (Recovery)
| Name: Recovery (Case-insensitive)
| Mount Point: Not Mounted
| Capacity Consumed: 1251835904 B (1.3 GB)
| Sealed: No
| FileVault: No
|
+-> Volume disk4s5 D71D56D7-3D99-4793-A1B0-05E96D1B76E9
| ---------------------------------------------------
| APFS Volume Disk (Role): disk4s5 (Data)
| Name: Data (Case-insensitive)
| Mount Point: /System/Volumes/Data
| Capacity Consumed: 3961692160 B (4.0 GB)
| Sealed: No
| FileVault: No
|
+-> Volume disk4s6 8E3E0F26-CE48-43AA-8D63-A59DC1A151FB
---------------------------------------------------
APFS Volume Disk (Role): disk4s6 (VM)
Name: VM (Case-insensitive)
Mount Point: /System/Volumes/VM
Capacity Consumed: 20480 B (20.5 KB)
Sealed: No
FileVault: No
(With the apfs list verb, diskutil gives us the APFS-native view: containers, physical stores, volumes, roles, snapshots, UUIDs, and space accounting.)
Focus on this detail:
+-> Volume disk4s1 C806F908-958F-4945-ADB7-29B5646526E9
| ---------------------------------------------------
| APFS Volume Disk (Role): disk4s1 (System)
| Name: Macintosh HD (Case-insensitive)
| Mount Point: Not Mounted
| Capacity Consumed: 12551831552 B (12.6 GB)
| Sealed: Yes
| FileVault: No
| |
| Snapshot: 7CF13368-3CF2-480F-8518-6626E86ACCFE
| Snapshot Disk: disk4s1s1
| Snapshot Mount Point: /
| Snapshot Sealed: Yes
which actually shows how the mechanism works. The system volume is not mounted. But it has a snapshot, whose UUID is 7CF13368-3CF2-480F-8518-6626E86ACCFE, the snapshot disk is disk4s1s1. The OS runs from that snapshot.
Usually, under Linux and BSD, the filesystem is mounted at /. The neat result is that the user perceives “the system” and the root filesystem as the same thing. On modern macOS, this is not true. Macintosh HD - or better, disk4s1 is there, it exists. It occupies roughly 12.6 GB of space. But it isn’t mounted anywhere. What is really mounted on / is disk4s1s1, the snapshot.
Let’s explicitly observe something. Snapshots and seals are not “marketing-named defences”, such as “Gatekeeper” or “System Integrity Protection”. They’re just technical primitive features of APFS that Apple has also used to build some defences. For instance, most users first met APFS snapshots through Time Machine.
In fact - an APFS snapshot is a
- point-in-time
- read only
- immutable version of a volume. It’s not a “real” copy, from this perspective; but since APFS is copy-on-write the snapshot shares blocks with the live volume until the user or an agent changes something. In that case, new blocks are allocated, but only for the delta. Initial cost: near zero. Time cost: O(delta). Plus the usual metadata tax.
This observation shows some technical properties that enforce the defensive character of the snapshot mechanism:
- read-only. You cannot write to it. Not even with
mount -uw. To modify a system running from a snapshot, one must modify the underlying System volume, create a new bootable snapshot, and make the system boot from it. With SSV, this also means dealing with the seal (we’ll get there, hold your horses). - atomic. The snapshot either exists or doesn’t exist: it sees a consistent point-in-time state, not a half-mutated filesystem. If you take a snapshot while you’re writing a file, the snapshot itself sees either the status before or after the writing operation. This results in integrity enforcement.
- cheap to revert. Rolling back to a snapshot comes almost for free. It’s one of the mechanisms behind the “Restore to previous version” functionality we see on many backup systems.
Sealing
A snapshot is immutable by construction, we saw. Sealing adds a cryptographic verification of its integrity.
Apple builds a Merkle-tree over the contents of the sealed System snapshot. Each block is hashed, the hashes are grouped in nodes, up to a root (of the tree… too many roots here.)
The root hash — the seal — is then signed/trusted as part of Apple’s boot chain.
That Sealed: Yes displayed by diskutil is APFS telling you: this object is not merely read-only; it has a cryptographic seal.
At boot time, depending on the Mac generation, iBoot or the boot chain verifies the seal before the snapshot becomes /. If the hash does not match, the seal is considered invalid and the normal boot process aborts. This is the main difference with “read-only mounting” a volume. A read-only volume can be written, provided one has the right privileges to do so. A sealed volume cannot - even if someone could write on it, the seal would be invalid at the next boot.
Putting it all together: SSV
If:
- the volume is sealed (
Sealed: Yes) - the snapshot is sealed (
Snapshot Sealed: Yes) - and
Snapshot Mount Point: /…then we have the so called Signed System Volume (SSV). It is not a different volume, but the configuration of a system volume and its snapshot, properly sealed. Apple calls it SSV.
The boot sequence becomes:
- iBoot (or the boot chain) verifies the kernelcache signature
- iBoot verifies the snapshot seal against Apple’s signing keys (the root of the Merkle tree)
- If the seal is valid, then
- kernel mounts the snapshot as
/
- kernel mounts the snapshot as
- otherwise, it rejects it.
When it comes to updating, the flow (very simplified, here) becomes:
- Apple signs a new system volume
- the updater writes on the “live” volume (i.e.
disk4s1), which is unmounted - a new snapshot is created
- its seal is verified
- if verification passes, the new snapshot becomes the boot snapshot and the system reboots into it. The old snapshot stays as fallback: if something goes wrong, you can still boot the previous OS state.
Some important observations are required, here.
First, to remove all ambiguities: it is important to state clearly that this is not SIP. Here two filesystem primitives (snapshot and seal) and an architectural choice (booting from the snapshot) have been assembled together to build another layer of defence, independent from SIP. SIP deals with the enforcement of kernel policies at runtime, whilst SSV is a structural property of the filesystem. They strengthen each other, but they are independent.
Second: the seal is not “every single file is signed by the OS”. The seal is just a hash, namely the root of the Merkle tree, that covers the entire volume. There is only one signature — well, Fibonacci and Recursion would probably object, but I trust the common sense of the reader. What changes is the underlying tree. This is important because it has impacts on performance. In fact, to check the seal at boot time it is sufficient to check the root against Apple’s signature. If something has been tampered with, the hashes don’t match. It’s not even important understanding where and why the hashes don’t match: the system does not boot anyway. Furthermore, APFS can verify subtrees on demand when it reads blocks. This means that even at runtime, reading a file from the system volume implies a cryptographic check from the path of the file up to the root. Therefore, a runtime tampering is immediately detected. Finally, if Apple had to sign every single file, each update would require a tremendous amount of signatures and a gigantic trust structure in place. With Merkle trees, only one item is signed.
Third, a security observation. During the years we saw several SIP bypasses (Shrootless CVE-2021-30892, “migraine” CVE-2023-32369, …). Breaking the SSV is a different thing. It would require breaking the underlying cryptographic algorithms, not “finding an entitlement that breaks policies”.
The canonical tool to observe the status of SSV is csrutil. By the way, it is also used to investigate the SIP status - which is probably what justifies some of the common misunderstandings about the two technologies.
Launched without arguments, as usual, it gives a short help page:
gabriel@LinuxFanboysSuck ~ % csrutil
usage: csrutil <command>
Modify the System Integrity Protection configuration.
Available commands:
clear
Clear the existing configuration.
disable
Disable the protection of the OS installation. Only available in
Recovery OS.
enable
Enable the protection of the OS installation. Only available in Recovery
OS.
status
In Recovery OS, displays the configuration for each OS installation.
In macOS, displays the configuration of the running OS.
allow-research-guests
status
Show the current allow research guests setting.
disable
Disallow research guests. Only available in Recovery OS.
enable
Allow research guests. Only available in Recovery OS.
authenticated-root
status
Show the current authenticated root setting.
disable
Allow booting from non-sealed system snapshots. Only available in
Recovery OS.
enable
Only allow booting from sealed system snapshots. Only available in
Recovery OS.
Observe how the most important features require booting in Recovery OS (keeping the power button pressed for a while during restart on Apple Silicon machines). In other words, the user must lower the security level to change these values.
On a fresh system, like the one I am using now:
gabriel@LinuxFanboysSuck ~ % csrutil authenticated-root
Authenticated Root status: enabled
(it looks like status is not required, actually).
However: Authenticated Root is the csrutil name for the SSV-related control. It is important also to read between the lines: the “authenticated” framing is stricter than “signed”, from a logical perspective. Actually, authenticated here means that the root — both the filesystem root and the Merkle-tree root; yes, enjoy the pun — is checked before being trusted. It is an active authentication, not a passive property.
Conclusions
If you were wondering: what happens if you ran sudo rm -fr / --no-preserve-root on a Mac? Well — not much, at least where the sealed system is concerned.
System applications remain mostly there:
gabriel@virtosaurus-erectus ~ % ls /System/Applications
App Store.app Font Book.app Music.app Siri.app
Apps.app Freeform.app News.app Stickies.app
Automator.app Games.app Notes.app Stocks.app
Books.app Home.app Passwords.app System Settings.app
Calculator.app Image Capture.app Phone.app TextEdit.app
Calendar.app Image Playground.app Photo Booth.app Time Machine.app
Chess.app iPhone Mirroring.app Photos.app Tips.app
Clock.app Journal.app Podcasts.app TV.app
Contacts.app Mail.app Preview.app Utilities
Dictionary.app Maps.app QuickTime Player.app VoiceMemos.app
FaceTime.app Messages.app Reminders.app Weather.app
FindMy.app Mission Control.app Shortcuts.app
/Applications takes some damage, nevertheless: the Utilities subdirectory is gone.
gabriel@virtosaurus-erectus ~ % ls /Applications
Safari.app
Unsurprisingly, the structure of the home folder does not change:
gabriel@virtosaurus-erectus ~ % ls ~/
Desktop Downloads Movies Pictures
Documents Library Music Public
This is just to give you the idea. However, I ran that rm on a machine with no SIP/SSV changes. The next time, I promise, I’ll bring you the results on a tweaked machine.
We’ll catch up in a week or so, with some more tests and a discussion on SIP. Or on the Fibonacci Soup. You’re welcome to cook it.
Stay paranoid, and have fun. Cook some Soup.