A kernel's ability to write page tables does not imply unrestricted access to pages protected by the Page Protection Layer (PPL). The boundary combines controlled entry points for ownership changes, permission updates, and exception recovery with hardware restrictions on other execution paths.
This analysis follows an ARM64 kernelcache from protected objects through service dispatch to a physical-page handoff. The device and build are unidentified, so the strides, CPU count, and service limit belong only to this sample. Apple's XNU 7195.60.75 sources provide a comparison for public software logic, not an exact source match for the binary.
Establish sizes before interpreting the layout
pmap_bootstrap converts available physical memory through phystokv and constructs a pmap array. Allocation multiplies the element count by 0x108 before rounding up to 0x4000. Each initialized element links to its predecessor, leaving the free-list head at the array's high end; allocation policy still requires examining removal from that list.
flowchart TD A["Available physical memory"] --> B["phystokv"] B --> C["pmap array / stride 0x108"] B --> D["Per-CPU data / stride 0x180"] B --> E["PPL stacks and save areas"] B --> F["Physical attributes and reverse mappings"]
This is an object relationship diagram, not a scaled contiguous memory map. The observed per-CPU initialization covers six records. Only the identified prefix is shown; no fields are invented for the remaining bytes.
| Offset | Name | Type | Size |
|---|---|---|---|
| 0x00 | saved_kernel_sp | 8 | |
| 0x08 | ppl_stack | 8 | |
| 0x10 | save_area | 8 | |
| 0x18 | ppl_state | 4 |
ADD ..., #4, LSL #12 adds 16 KiB; #8, LSL #12 adds 32 KiB. Reading the immediate 4 as 4 KiB misidentifies both stack sizing and page advancement. Reserved spacing alone does not establish an unmapped guard page; that requires inspecting the resulting mappings.
Ownership checks also cover output buffers
| Bits | Field | Value |
|---|---|---|
| 15 | PP_ATTR_NO_MONITOR | 0 |
| 14 | PP_ATTR_MONITOR | 1 |
| 13–0 | other_attributes |
pp_attr_table holds 16-bit physical-page attributes. MONITOR denotes PPL ownership; NO_MONITOR prevents a page from becoming PPL-owned while that constraint is active. The set bit above illustrates an owned page, not a captured memory value.
The second attribute closes a time-of-check/time-of-use window. While a service writes to an ordinary kernel output buffer, the page's ownership must remain stable; otherwise an ordinary output operation could become a write into protected memory. The output-parameter pinning and ownership paths in public pmap.c support this interpretation.
flowchart TD P["Physical page"] --> A["pp_attr_table attributes"] P --> H["pv_head_table entry and lock"] H --> V1["PTE mapping the page"] H --> V2["PTE in another address space"] V1 --> M1["Virtual view A"] V2 --> M2["Virtual view B"]
Reverse-mapping metadata tracks mappings from a physical page; it is not another flat list of virtual addresses. Read the ownership attributes, mapping checks, and PVH lock together. Setting an ownership bit does not mean every PTE permission has already changed.
Installing an entry point is not entering it
| Stage | Observable action | Interpretation limit |
|---|---|---|
| bootstrap | Write the bootstrap handler to S3_6_C15_C8_1 |
Install an entry, rather than call it |
| Early exception entry | Write a deadloop to S3_6_C15_C8_2 |
Do not treat the placeholder as the final handler |
| lockdown | Set pmap_ppl_locked_down = 1 and enter gxf_enable |
Separate the software flag from subsequent hardware operations |
| First protected entry | Install the normal PPL handler and exception vectors | Switch from boot configuration to service dispatch |
Machine words 0x00201420 and 0x00201400 appear on the sample's entry and exit paths. Surrounding control flow supports interpreting them as protected entry and exit operations in this implementation. An undecoded word alone does not establish the complete private hardware semantics.
Initial handling also uses MPIDR_EL1 to locate the current CPU record, prepares the save area, and adjusts related registers. Testing a private register's low bit proves only that the path requires that bit. Masking SPSR_EL1 is not evidence of a standard EL3 transition: GXF protected state should not be equated with an extra architectural exception level.
Normal dispatch differs from exception recovery
Wrappers place the service number in X15. The unsigned check CMP X15, #0x47; B.CS ... accepts 0x00–0x46, or 71 service numbers; 0x47 is already out of range. The table entry is at table_base + X15 * 8. BLRAA X10, X9 additionally uses the entry address as the pointer-authentication modifier.
Before lockdown, bootstrap dispatch calls through the table directly. After lockdown, normal entry carries W10 = 0 and requires the current CPU to be in KERNEL state. Only then does it save the caller's SP, switch to the PPL stack, and dispatch.
stateDiagram-v2 KERNEL --> DISPATCH: W10 = 0 / save kernel SP DISPATCH --> KERNEL: normal return / restore kernel SP DISPATCH --> EXCEPTION: save protected context EXCEPTION --> DISPATCH: W10 = 3 / restore save area
The states shown are 0, 1, and 3; the diagram includes only the paths discussed here. They describe the current CPU. Finding DISPATCH calls for examining reentry or exception handling, not assuming that another CPU is using the service.
The synchronous exception path saves context through the pointer at offset 0x10, reuses ordinary kernel fleh_synchronous handling, and passes X26 = 1 as the origin marker. Reentry with W10 = 3 must match EXCEPTION state before restoring the interrupted context. This is not a new service invocation, nor is its context reconstructed from the normal PPL stack at offset 0x08.
A page handoff has several validation layers
The essential attribute update in pmap_mark_page_as_ppl_page_internal is old_attr | 0x4000 through a 16-bit compare-and-swap. LDRSH sign-extends the attribute so the path can recognize 0x8000. CASALH is not a plain store; a failed comparison must return to validation. Public source also checks existing PPL ownership and disallowed mappings.
The sample then calls pmap_set_range_xprr_perm(va, va + 0x4000, 3, 1) for the direct mapping. The expected old permission is a condition to verify, not merely a hint for an unconditional overwrite.
- 1
Validate the range
Require 16 KiB alignment, a start no greater than the end, and an allowed direct-map or static region. Read endpoint comparisons from the relevant build.
- 2
Find leaf entries
Walk translation descriptors and distinguish table entries from other types. Convert valid physical addresses into accessible kernel addresses.
- 3
Lock and recheck
Lock the physical page's PVH entry, reload the PTE, and validate its type, hint bit, and expected old permission.
- 4
Update and finish
Preserve unrelated bits, insert the new permission combination, issue barriers, release the lock, and advance the PTE and virtual address.
The extraction below uses X8 for the PTE and X26 for the expected permission. The immediate 0x35 is decimal 53.
LSR X9, X8, #4
AND X9, X9, #0xC
LSR X10, X8, #0x35
BFXIL X9, X8, #0x35, #1
AND X10, X10, #2
ORR X9, X9, X10
CMP X9, X26The loop tail advances an 8-byte PTE pointer and a 16 KiB virtual-page address. LDCLRL atomically clears the selected mask bits; it does not retain only those bits.
DSB ISH
LDCLRL W21, W8, [X8]
ADD X20, X20, #8
ADD X25, X25, #4, LSL #12
CMP X20, X27Separate permission numbers, indices, and field values
The sample combines AP and the two execute-never bits directly into a four-bit value. This extraction is not a universal xPRR encoding. The public XNU APRR path first applies PTE_TO_APRR_INDEX and then maps the result through pte_to_xprr_perm to a software permission number. Those two numbers need not be identical.
| Operation | Sample transition | Semantic check |
|---|---|---|
| Adopt an ordinary page | 3 → 1 |
Replace the ordinary kernel-writable view with a PPL-managed writable view |
| Release a PPL page | 1 → 3 |
Remove the corresponding ownership and restore kernel-side use |
| Protect PPL text | 0xA → 0x8 |
Preserve PPL execution while restricting ordinary kernel writes and execution |
Public source describes the PPL text target as RX for PPL and RO for the kernel. That explains the need for two views of one physical page; it does not prove that every hardware register uses identical fields at every boot stage. Record dual-view permission configuration fields, PTE-derived indices, and XPRR_*_PERM software numbers separately.
Check arithmetic without claiming hardware validation
This model checks extraction and insertion for the sample, preservation of unrelated bits, allocation rounding, service bounds, and attribute setting. It neither accesses page tables nor executes private instructions or reproduces CPU concurrency.
PAGE = 0x4000
MASK64 = (1 << 64) - 1
PTE_MASK = (3 << 6) | (3 << 53)
def sample_index(pte):
return ((pte >> 4) & 0xC) | ((pte >> 53) & 3)
def sample_bits(index):
return ((index & 0xC) << 4) | ((index & 3) << 53)
for index in range(16):
for seed in (0, MASK64, 0x123456789ABCDEF0):
original = seed & MASK64
updated = (original & ~PTE_MASK) | sample_bits(index)
assert sample_index(updated) == index
assert (updated & ~PTE_MASK) == (original & ~PTE_MASK)
assert sample_index((2 << 6) | (1 << 53)) == 9
assert sample_index((3 << 6) | (1 << 54)) == 14
assert (4 << 12, 8 << 12) == (PAGE, PAGE * 2)
for count in range(1025):
size = (count * 0x108 + PAGE - 1) & ~(PAGE - 1)
assert size % PAGE == 0 and size >= count * 0x108
assert size - count * 0x108 < PAGE
assert [i for i in range(0x48) if i < 0x47] == list(range(71))
for old in range(0x8000):
assert (old | 0x4000) & 0x4000
assert ((old | 0x4000) & ~0x4000) == (old & ~0x4000)
print("PASS: 48 PTE round trips; 1025 alignments; 71 service IDs; 32768 attribute cases")The run passes 48 PTE round trips, 1025 alignment cases, 71 valid service numbers, and 32768 attribute inputs. These checks catch shifts, masks, and bounds errors. They do not validate page-table ordering, TLB synchronization, or private register configuration on hardware.
Evaluate the PPL boundary in four stages: valid entry, matching per-CPU state, stable ownership and mappings, and the hardware interpretation of the resulting index. Explaining only one state bit or one PTE number misses the other layers that constrain the write.
Official references
Apple Platform Security describes the protection goal. These fixed-version source files provide public software cross-checks, separate from the unidentified kernelcache build.