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

KTRR 与 CTRR:保护范围和锁定时序

从启动映射收缩追踪到 CTRR 锁定,核对物理端点、KTRR 与 CTRR 的范围差异、分代 TLB 时序、调试位限制和多核 reset 条件,避免把单次寄存器写入当成完整保护证据。

date[31:24]
read[23:16]
6 分钟
cat[15:8]
移动端
目录
  1. 0x00启动映射只保留打开 MMU 所需的一页
  2. 0x01设备树属于权限准备,不是普通代码段
  3. 0x02KTRR 与 CTRR 的终点不应混用
  4. 0x03CTRR 配置要保留屏障与分代条件
  5. 0x04调试位限制不是一次寄存器回读
  6. 0x05reset 路径先确认前提,再决定是否重配
  7. 0x06离线核对边界与分支
  8. 0x07官方参考

KTRR / CTRR 的保护边界不只由最后一次锁寄存器写入决定。启动映射覆盖了什么、只读区间如何换算、TLB 何时失效,以及其他 CPU 如何确认锁定状态,都影响那次写入的实际意义。

下面围绕 2021 年 ARM64 内核分析中的 CTRR 路径展开,并以 Apple XNU 7195.60.75 固定版本源码核对分支。所分析二进制的设备与完整构建标识缺失;源码对应关系是交叉验证,不是对二进制身份的认定。KTRR 与 CTRR 共用部分启动逻辑,但保护范围和寄存器序列仍需分别讨论。

启动映射只保留打开 MMU 所需的一页

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

掩码清除最低 14 位,得到 16 KiB 页边界。接下来把启动代码的虚拟地址转换为物理地址,并沿启动页表定位相应表项。mmu_kvtop 与 phystokv 处于相反的转换方向,PTE 中的物理地址应与用于访问表页的内核虚拟地址分开记录。

公开 arm_replace_identity_map 的目的很具体:把早期大范围 V=P 映射收缩为打开 MMU 所需的映射,避免可执行 block 跨越硬件保护边界。它取得 L1、L2 表页,分配新的 L3 表页,清零 L1、L2,再仅连接相关条目;这不是把所有启动页表无条件清空。

样本中的地址拆分4 行
层级 取位 宽度 索引覆盖
L1 bits 36–38 3 8 个索引
L2 bits 25–35 11 2048 个索引
L3 bits 14–24 11 2048 个索引
页内偏移 bits 0–13 14 16 KiB

ADD ..., #4, LSL #12 的步长同样为 0x4000。一张 16 KiB 表容纳 2048 个 8 字节表项;顶层只使用部分索引,并不表示顶层分配只有 8 个条目的空间。样本末级属性常量 0x40000000000683 应结合翻译配置解释,而不是独立当成“永远可执行”的结论。

设备树属于权限准备,不是普通代码段

arm_vm_prot_init 除了处理 Mach-O 段,还根据设备树的锁定状态调整 segEXTRADATA。固定版本源码在 SecureDTIsLockedDown() 成立时采用 PE_state.deviceTreeHead 与 deviceTreeSize,并包含 TrustCache 对额外数据范围的影响,随后配置 RNX 映射。

这里有两个不同的问题:设备树是否已锁定,以及额外数据区域位于什么地址。后者还由 segEXTRADATA、segLOWEST 与 segLOWESTRO 的比较决定,不应把这些条件都归入 SecureDTIsLockedDown 的返回值。该函数名也不足以证明设备树与内核映像之间的具体大小关系。

因此,保护范围分析应同时保留映像段表与启动传入数据区域。只列出 __TEXT 和 __DATA 会丢掉影响最低只读边界的对象。

KTRR 与 CTRR 的终点不应混用

rorgn_stash_range 先计算并保存范围,rorgn_lockdown 随后才配置硬件。公开实现还比较 iBoot 已设置的 AMCC 区域与 XNU 计算结果,避免两侧对边界各自作出不同解释。

XNU 7195.60.75 的范围差异3 行
配置 MMU 保护终点 与 AMCC 的关系
KTRR kvtophys(segLASTB) - segSizeLASTDATACONST - 1 __LAST 在 AMCC 只读区域内,但不在 MMU KTRR 区域内
CTRR,有 segHIGHESTRO kvtophys(segHIGHESTRO) - 1 核对 CTRR 与 AMCC 起止相同
CTRR,无 segHIGHESTRO kvtophys(segLASTB) + segSizeLAST - 1 同样包含 __LAST 并核对边界

两者起点都由 kvtophys(segLOWESTRO) 得到。这里使用的 ctrr_begin、ctrr_end 是首字节与末字节均包含的物理端点;[start, end_exclusive) 应转为 [start, end_exclusive - 1]。空区间、地址回绕与非连续转换应在计算前排除,不应把任意虚拟地址直接送入硬件。

源码特别指出,硬件端点的处理粒度与处理器支持的最小页粒度有关,并不简单等同于当前 16 KiB 页大小。因此,“最后一页起点”和“最后一个字节”也不是可互换的输入。

CTRR 配置要保留屏障与分代条件

CTRR 寄存器分工4 行
用途 EL1 编码 EL2 集群配置编码
起点 S3_4_C15_C2_3 S3_4_C15_C11_0
终点 S3_4_C15_C2_4 S3_4_C15_C11_1
控制 S3_4_C15_C2_5 S3_4_C15_C11_4
锁定 S3_4_C15_C2_2 S3_4_C15_C11_5

头文件将控制值 0x12 分解为两个启用位。其他字段被列出,不代表这次写入也启用了它们。

CTRR_CTL_EL1 — 0x128 位
B_UXNA_UXNB_PXNA_PXNB_MMUON_WRPROTECTB_MMUOFF_WRPROTECTA_MMUON_WRPROTECTA_MMUOFF_WRPROTECT
值0x12= 18
位字段值
7B_UXN0
6A_UXN0
5B_PXN0
4A_PXN1
3B_MMUON_WRPROTECT0
2B_MMUOFF_WRPROTECT0
1A_MMUON_WRPROTECT1
0A_MMUOFF_WRPROTECT0

lock_mmu 先写边界,再写控制值,最后写 1 锁定。这个概括仍不足以替代实际时序:在所核对版本中,非 APPLEVORTEX 路径于边界写入后执行 ISB 与 TLB 失效;APPLEVORTEX 路径则在后面的 ISB 之后刷新。跨芯片复用寄存器序列时,省略这一分支会丢掉必要语义。

CurrentEL == 8 对应 EL2 编码,该条件成立时才写另一组集群配置寄存器。函数包含 EL2 分支,并不证明所分析设备在特定启动方式下执行过它。AMCC/IOA 锁组与 MMU 范围也属于不同配置对象,不应将一次 MSR 概括为全部保护均已完成。

调试位限制不是一次寄存器回读

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

该变换按调用参数清位、置位,随后强制清除 MDSCR_EL1.KDE 对应的 bit 13。固定版本的 update_mdscr 在写入后测试的是通用寄存器 X2,不是再次执行 MRS MDSCR_EL1 的硬件回读。这一区别直接影响“验证写入成功”之类结论是否成立。

源码把异常 KDE 状态与清位、重试及 MDSCR.KDE was set 的 panic 路径相连,其设计还需结合断点控制寄存器约束理解。沿正常顺序执行时,先前清位会让该测试为零;附加路径的意义包括约束从中间进入的异常控制流。仅看这段代码,不等于已经复现调试机制突破只读保护。

reset 路径先确认前提,再决定是否重配

XNU start.s 中的相关分支4 步
  1. 1

    检查 lockdown 完成状态

    相关 CPU reset 路径先检查 lockdown_done;尚未完成时原地等待,不继续配置。

  2. 2

    读取保存范围

    起止值为零时,DEBUG、DEVELOPMENT 或 CONFIG_DTRACE 分支跳过 CTRR 配置;其他配置在该条件下原地等待。

  3. 3

    检查现有锁状态

    已经锁定时跳过重复写入;未锁定时才写边界、控制值和锁定位。

  4. 4

    同步并确认

    执行相应屏障和 TLB 操作,再等待锁定状态。集群主核的启动顺序与锁定职责也属于这条协议。

这描述的是特定 CPU 进入路径,而不是“每次系统重启后的第一条操作”。所核对源码区分引导集群与其他集群,并依赖启动序列化。保留这些条件,才能解释为何某个 CPU 跳过配置仍然可能是正确行为。

离线核对边界与分支

下列模型只检查低 39 位地址的索引重组、端点算术、控制掩码和简化分支,不访问系统寄存器。模型中的数值是明确的测试输入,不是设备地址。

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")

结果为 8 组地址、9 组端点、5 个 reset 分支及掩码检查通过。它没有验证硬件锁定、缓存一致性或真实设备启动;这些需要相应映像、构建配置、寄存器观测和页表数据。

最终应沿“保留哪些映射 → 哪些数据影响范围 → 使用哪一种端点 → 按何种时序锁定 → 其他 CPU 如何确认”的链条审阅证据。KTRR 与 CTRR 的差异必须在链条中显式保留,而不是只在标题里并列两个缩写。

官方参考

以下均为 Apple 发布的固定版本文件,分别覆盖页表、范围、寄存器定义、调试位与 reset 路径。

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