KTRR and CTRR boundaries are not determined solely by the final lock-register write. Boot mappings, range conversion, TLB invalidation, and the way other CPUs confirm lockdown all affect what that write achieves.
The analysis follows the CTRR path in a 2021 ARM64 kernel study and cross-checks branches against Apple's XNU 7195.60.75. The binary's device and full build identity are missing; matching source logic does not identify the binary. KTRR and CTRR share some startup code, but their ranges and register sequences still require separate treatment.
Retain only the page needed to enable the MMU
ADRL X8, _bootstrap_instructions
AND X21, X8, #0xFFFFFFFFFFFFC000
MOV X0, X21
BL _mmu_kvtopThe mask clears the low 14 bits, aligning to a 16 KiB page. The path converts the boot code's virtual address to a physical address and locates its translation entries. mmu_kvtop and phystokv perform opposite conversions: distinguish the physical address in a descriptor from the kernel virtual address used to access its table.
Public arm_replace_identity_map has a precise purpose: narrow the early V=P mapping to what is needed to enable the MMU, avoiding executable blocks that cross a hardware protection boundary. It obtains the L1 and L2 pages, allocates an L3 page, clears L1 and L2, and reconnects the relevant entries. It does not indiscriminately erase every boot page table.
| Level | Bits | Width | Index coverage |
|---|---|---|---|
| L1 | bits 36–38 | 3 | 8 indices |
| L2 | bits 25–35 | 11 | 2048 indices |
| L3 | bits 14–24 | 11 | 2048 indices |
| Page offset | bits 0–13 | 14 | 16 KiB |
ADD ..., #4, LSL #12 also advances by 0x4000. A 16 KiB table accommodates 2048 eight-byte entries; using only part of the top-level index space does not mean allocating space for just eight entries. Interpret the sample's leaf constant 0x40000000000683 with its translation configuration, not as a standalone promise of permanent executability.
The device tree participates in permission preparation
Besides Mach-O segments, arm_vm_prot_init adjusts segEXTRADATA based on device-tree lockdown. In the fixed source version, a true SecureDTIsLockedDown() selects PE_state.deviceTreeHead and deviceTreeSize. TrustCache data can also affect the extra-data range before RNX mappings are configured.
Device-tree lockdown and the location of extra data are separate questions. Comparisons involving segEXTRADATA, segLOWEST, and segLOWESTRO address the latter; those conditions should not all be attributed to the return value of SecureDTIsLockedDown. The function name alone establishes no ordering between device-tree and kernel-image addresses.
Keep both the image's segment layout and its boot-supplied data regions when analyzing the range. A list containing only __TEXT and __DATA omits objects that can affect the lowest read-only boundary.
KTRR and CTRR do not share every endpoint
rorgn_stash_range computes and saves bounds before rorgn_lockdown configures hardware. The public implementation also checks AMCC bounds set by iBoot against XNU's calculation, rather than allowing the two sides to interpret the range independently.
| Configuration | MMU protection endpoint | Relationship to AMCC |
|---|---|---|
| KTRR | kvtophys(segLASTB) - segSizeLASTDATACONST - 1 |
__LAST is inside the AMCC read-only range but outside the MMU KTRR range |
CTRR with segHIGHESTRO |
kvtophys(segHIGHESTRO) - 1 |
Check that CTRR and AMCC bounds match |
CTRR without segHIGHESTRO |
kvtophys(segLASTB) + segSizeLAST - 1 |
Also include __LAST and check the bounds |
Both start at kvtophys(segLOWESTRO). The saved ctrr_begin and ctrr_end are physical bounds inclusive of the first and last byte: translate [start, end_exclusive) into [start, end_exclusive - 1]. Rule out empty ranges, wrapping arithmetic, and discontinuous conversions before computing endpoints; an arbitrary virtual address is not a hardware-ready bound.
The source also distinguishes the processor's minimum supported granule from the currently used 16 KiB page size when discussing hardware endpoint handling. The start of the last page and the last byte are not interchangeable inputs.
Preserve barriers and generation-specific sequencing
| Role | EL1 encoding | EL2 cluster configuration encoding |
|---|---|---|
| Lower bound | S3_4_C15_C2_3 |
S3_4_C15_C11_0 |
| Upper bound | S3_4_C15_C2_4 |
S3_4_C15_C11_1 |
| Control | S3_4_C15_C2_5 |
S3_4_C15_C11_4 |
| Lock | S3_4_C15_C2_2 |
S3_4_C15_C11_5 |
The header decomposes 0x12 into two enabled control bits. Listing the other fields does not mean that this write enables them.
| Bits | Field | Value |
|---|---|---|
| 7 | B_UXN | 0 |
| 6 | A_UXN | 0 |
| 5 | B_PXN | 0 |
| 4 | A_PXN | 1 |
| 3 | B_MMUON_WRPROTECT | 0 |
| 2 | B_MMUOFF_WRPROTECT | 0 |
| 1 | A_MMUON_WRPROTECT | 1 |
| 0 | A_MMUOFF_WRPROTECT | 0 |
lock_mmu writes bounds, then control, then 1 to lock. That summary still omits a material sequencing condition: in the checked version, non-APPLEVORTEX builds issue ISB and invalidate the TLB after writing bounds. The APPLEVORTEX path flushes after a later ISB. Reusing a register sequence across generations while dropping this branch loses part of its meaning.
CurrentEL == 8 encodes EL2; the additional cluster registers are written only under that condition. A branch's presence does not prove that a particular device executed it during a particular boot. AMCC/IOA lock groups and MMU protection ranges are also separate configuration objects: one MSR does not establish that every layer is locked.
A debug-bit check is not a register readback
BIC X2, X2, X0
ORR X2, X2, X1
AND X2, X2, #0xFFFFFFFFFFFFDFFFThe transformation clears and sets requested bits, then forcibly clears bit 13, MDSCR_EL1.KDE. In the fixed source version, update_mdscr tests general-purpose register X2 after the write; it does not issue another MRS MDSCR_EL1 to read hardware back. This matters when assessing claims that the path verifies a completed register write.
The source connects an unexpected KDE state to clearing, retrying, and a panic carrying MDSCR.KDE was set. Its design also depends on breakpoint-control constraints. Sequential entry through the earlier clear makes the tested bit zero; the extra path also constrains abnormal control flow entering partway through. This code alone is not a reproduced debug-based break of read-only protection.
Reset entry checks prerequisites before reconfiguration
- 1
Check lockdown completion
The relevant CPU reset path first checks
lockdown_done. It spins rather than continuing when lockdown is incomplete. - 2
Read saved bounds
A zero endpoint skips CTRR setup under DEBUG, DEVELOPMENT, or CONFIG_DTRACE. Other configurations spin on that condition.
- 3
Check the existing lock
An already locked state skips duplicate writes. Otherwise, write bounds, control, and the lock.
- 4
Synchronize and confirm
Perform the relevant barriers and TLB operations, then wait for the locked state. Cluster-master ordering and responsibility are part of this protocol.
This is a conditional CPU-entry path, not a claim about the first instruction after every system reboot. The checked source distinguishes the boot cluster from other clusters and relies on serialized startup. Retaining these conditions explains why a CPU that skips configuration may still be behaving correctly.
Check bounds and branches offline
The model below checks address reconstruction in the low 39 bits, endpoint arithmetic, control masks, and simplified branches. It does not access system registers. Its numeric values are explicit test inputs, not device addresses.
PAGE = 1 << 14
MASK64 = (1 << 64) - 1
def split_address(addr):
return ((addr >> 36) & 7, (addr >> 25) & 0x7FF,
(addr >> 14) & 0x7FF, addr & 0x3FFF)
cases = [0, PAGE - 1, PAGE, (1 << 25) - 1, 1 << 25,
(1 << 36) - 1, 1 << 36, (1 << 39) - 1]
for addr in cases:
l1, l2, l3, offset = split_address(addr)
assert (l1 << 36) | (l2 << 25) | (l3 << 14) | offset == addr
assert (addr & ~0x3FFF) + offset == addr
assert (4 << 12) == PAGE
assert MASK64 & ~0x3FFF == 0xFFFFFFFFFFFFC000
assert ((1 << 1) | (1 << 4)) == 0x12
assert MASK64 & ~(1 << 13) == 0xFFFFFFFFFFFFDFFF
assert (8 >> 2) & 3 == 2
for base in (0, PAGE, 0x100000):
for length in (1, PAGE, PAGE * 2):
end_inclusive = base + length - 1
assert end_inclusive - base + 1 == length
def reset_action(done, begin, end, locked, diagnostic):
if not done: return "spin"
if not begin or not end: return "skip" if diagnostic else "spin"
return "skip" if locked else "configure"
assert reset_action(False, 1, 2, False, True) == "spin"
assert reset_action(True, 0, 2, False, True) == "skip"
assert reset_action(True, 0, 2, False, False) == "spin"
assert reset_action(True, 1, 2, True, False) == "skip"
assert reset_action(True, 1, 2, False, False) == "configure"
print("PASS: 8 address cases; 9 endpoint cases; 5 reset branches; masks and EL encoding")The run passes eight address cases, nine endpoint cases, five reset branches, and mask checks. It does not validate hardware lockdown, cache coherence, or an actual device boot. Those require the corresponding image, build configuration, register observations, and page-table data.
Follow the chain from retained mappings to range-affecting data, endpoint representation, lockdown order, and confirmation by other CPUs. Keep KTRR and CTRR differences explicit throughout that chain rather than placing both abbreviations in a title and treating them as interchangeable.
Official references
These fixed-version Apple files cover page tables, ranges, register definitions, debug control, and reset entry.