Introduction

Over a year ago siguza published a write-up about Apple’s APRR - a custom ARM extension that redefines pagetable permissions and protects certain parts of the kernel from itself. Since then Apple has released their M1 chip which not only features an updated version of APRR but also easily allows to run bare-metal code shortly after boot. There have been some rumors about the new version but nothing specific has been (publicly) documented yet.

Time to change that!

The first part of this post is a very brief introduction to memory management, pagetables, and user/kernel mode on aarch64. It’ll also summarize APRR which is the equivalent feature on previous Apple SoCs. You’ll be bored if you are already aware of these and should probably just skip the beginning.

Then we can finally get to the major part: Reverse engineering how SPRR and GXF work and what they do. This part will be interesting to you if you want to learn how I approached this challenge. If you on the other hand only want to understand what SPRR and GXF are feel free to head over to the Asahi Linux wiki directly!

MMUs, pagetables, and kernels

On ARM the CPU runs in what is called exception levels. If you’re familiar with x86 these are called rings instead. EL0 is userspace where applications run, EL1 is (usually) where the kernel itself runs and EL2 is where a hypervisor runs. (There is also EL3 for firmware or Trust Zone but the M1 doesn’t have that level)

On ARM64 CPUs with Virtualization Host Extensions there’s also a way to make EL2 look like EL1 such that a kernel can easily run there as well.

One of the kernel’s tasks is to lie to each application running in userland and to tell them that they’re the only one in the address space. It does that by using the memory management unit. The MMU allows creating an alias from a virtual address to a real physical address in e.g. RAM. The smallest granularity of this mapping is called a page which is usually 4KiB large. Each page has a virtual address and a physical address. When an instruction of an application or the kernel itself now tries to access memory at location x the MMU looks up the page in its pagetable and instead returns memory from another address y. And that’s how the kernel can give each userland application its own separate address space: It just creates a different set of pagetables for each process.

In addition to this mapping, each page also contains four bits that encode certain access flags. These flags determine if it’s possible to read from a page, write to a page or execute code from a page for a userland application or the kernel itself. The following four bits can be found in each pagetable entry on ARMv8-A CPUs:

There’s also some additional complexity related to determining the final access flags (PAN, hierarchical control) which I’ll ignore for this blog post. One thing to note here is that userland and kernel permissions are tightly coupled. It’s impossible to create a page that’s rw- in userland but r- for the kernel.

APRR

As mentioned, there are four flags for each page that control the access permissions (read/write/execute) for EL0/1 (user/kernel mode). APRR changes this behavior completely: Instead of storing the four flags as bits inside the page table entry, the four bits are repurposed as an index to a separate table (i.e. instead of encoding access permissions directly the bits are merged into a 4 bit index as [AP1][AP0][PXN][UXN]). This separate table then encodes the actual permissions of the pages. Additionally, some registers allow to further restrict these permissions for userspace. These registers are also separate for kernel and userland and allow much flexibility when creating page permissions.

APRR introduces a layer of indirection to pagetable permissions this way which allows to very efficiently flip the access permissions of many pages at once with a single register write. Usually, this would require a rather expensive page walk to modify all individual entries.

More details are available in siguza’s excellent write-up.

Just-In-Time Compilers

Usually, applications are compiled from a higher language to machine code which is then distributed. The code can easily be mapped as r-x since it’s fixed and usually won’t be modified during runtime anymore.