~/posts/mobile/ios-ktrr-ctrr-ranges-lockdown-order.md

KTRR and CTRR: ranges and lockdown order

Follow boot-map reduction through CTRR lockdown. Check physical endpoints, KTRR versus CTRR ranges, generation-specific TLB sequencing, debug controls, and conditional CPU reset entry.

date[31:24]
read[23:16]
7 min
cat[15:8]
Mobile
Contents
  1. 0x00Retain only the page needed to enable the MMU
  2. 0x01The device tree participates in permission preparation
  3. 0x02KTRR and CTRR do not share every endpoint
  4. 0x03Preserve barriers and generation-specific sequencing
  5. 0x04A debug-bit check is not a register readback
  6. 0x05Reset entry checks prerequisites before reconfiguration
  7. 0x06Check bounds and branches offline
  8. 0x07Official references

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

arm_vm_init — alignment excerptasm
ADRL X8, _bootstrap_instructions
AND  X21, X8, #0xFFFFFFFFFFFFC000
MOV  X0, X21
BL   _mmu_kvtop

The 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.

Address decomposition in the sample4 rows
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.

Range differences in XNU 7195.60.753 rows
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

CTRR register roles4 rows
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.

CTRR_CTL_EL1 — 0x128-bit
B_UXNA_UXNB_PXNA_PXNB_MMUON_WRPROTECTB_MMUOFF_WRPROTECTA_MMUON_WRPROTECTA_MMUOFF_WRPROTECT
value0x12= 18
BitsFieldValue
7B_UXN0
6A_UXN0
5B_PXN0
4A_PXN1
3B_MMUON_WRPROTECT0
2B_MMUOFF_WRPROTECT0
1A_MMUON_WRPROTECT1
0A_MMUOFF_WRPROTECT0

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

update_mdscr — value transformationasm
BIC X2, X2, X0
ORR X2, X2, X1
AND X2, X2, #0xFFFFFFFFFFFFDFFF

The 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

Relevant branches in XNU start.s4 steps
  1. 1

    Check lockdown completion

    The relevant CPU reset path first checks lockdown_done. It spins rather than continuing when lockdown is incomplete.

  2. 2

    Read saved bounds

    A zero endpoint skips CTRR setup under DEBUG, DEVELOPMENT, or CONFIG_DTRACE. Other configurations spin on that condition.

  3. 3

    Check the existing lock

    An already locked state skips duplicate writes. Otherwise, write bounds, control, and the lock.

  4. 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.

ctrr-boundary-model.pypython
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.

NORMAL~/posts/mobile/ios-ktrr-ctrr-ranges-lockdown-order.md§--
0%en