ARMv8.3 的 PAC 特性

名词释义:

  • PAC: Pointer Authentication Code
  • PA: Pointer Authentication
  • SCTLR: System ConTroL Register

该特性为指针值提供【签名】和【校验】的能力。ARM64 硬件上提供 5 个秘钥,分别用于为三种地址生成 PAC:

  • 代码地址:API{A,B}Key
  • 数据地址:APD{A,B}Key
  • 通用地址:APGAKey

PAC 的值是关于以下三者的函数:1、指针本身;2、上下文相关值(也称 Modifier)1;3、秘钥。

pac_calculation

生成后,PAC 会被附在指针值上(即“签在上面”),其所处的位置和长度与当前处理器:1)设置的虚拟地址长度以及;2)是否使能了 Address Tagging(即 TBI 特性)有关:

  • 使能了 Address Tagging:
  • 未使能 Address Tagging:

详见《Arm® Architecture Reference Manual for A-profile architecture》 (发布号 I.a)中的 D8.8.1 PAC field 一节。

值得一提的是,在不支持该特性的低版本处理器上,相关指令会被当作 nop 来执行。这一点在高通的文档 上有提及。

利用方式

目前最常见的利用方式如下:

  1. 压栈保存时用 paciasp,基于 APIAKey 和 SP 对 LR 进行签名;
  2. 出栈使用时用 autiasp,基于 APIAKey 和 SP 对 LR 进行校验。

该方式实现了一种后向 CFI(Backward-edge CFI),即针对返回地址的防护。除此之外,有的厂商(如苹果的 Mac 系统)还利用 PAC 实现前向 CFI(Forward-edge CFI),针对函数间接调用的防护;以及 DFI,针对重要数据指针的防护。

若要为某个程序开启这类保护,需要使用合适版本的编译器,并在编译构建时添加编译参数 -mbranch-protection=(可选值请参考编译器手册,如这里 )。如此,pac*/aut* 系列指令就会由编译器安插在程序里适当的位置。可参考 Linux 内核的 PAC 编译支持:commit 74afda4016a7 (“arm64: compile the kernel with ptrauth return address signing”)

Linux 内核利用现状

Linux 内核引入 ARMv8.3 Pointer Authentication 的初始补丁集:(v7) arm64: return address signing

当前 Linux 内核仅实现了上述基于 PAC 的后向 CFI 方案,配有两个 configs:

  • ARM64_PTR_AUTH:支持用户态 PA,在各进程执行 exec() 时初始化秘钥
  • ARM64_PTR_AUTH_KERNEL2:(在编译器支持的情况下)使能内核态 PA,内核本身会使能对返回地址的保护

具体使用方式如内核文档 所述:

When CONFIG_ARM64_PTR_AUTH is selected, and relevant HW support is present, the kernel will assign random key values to each process at exec*() time. The keys are shared by all threads within the process, and are preserved across fork().

对上述的 5 个秘钥,内核态会使用 IA(若内核态 PA 开启的话);另外 update_sctlr_el1() 的注释也有提到:在内核态中 EnIA3 不能被关闭,仅当从内核态退出至用户态内核退出(即类似关机这样的场景)4时才可能按需关闭。

1void update_sctlr_el1(u64 sctlr)
2{
3	/*                                                                      
4	 * EnIA must not be cleared while in the kernel as this is necessary for
5	 * in-kernel PAC. It will be cleared on kernel exit if needed.
6	 */                                                                     
7	sysreg_clear_set(sctlr_el1, SCTLR_USER_MASK & ~SCTLR_ELx_ENIA, sctlr);
8	// ...
9}

一个陷阱

在 Linux 内核中,每个进程都有各自的一套 PA 秘钥,每次进程切换都涉及秘钥切换。而秘钥切换存在这样一个问题:若在一个使能了 PA 的函数执行过程中切换新的秘钥,

 1paciasp
 2stp fp, lr, [sp, #-FRAME_SIZE]!
 3mov fp, sp
 4
 5/* ... function body ... */
 6pa_switch_keys
 7/* ... function body ... */
 8
 9ldp fp, lr, [sp], #FRAME_SIZE
10autiasp
11ret

那么在函数准备返回时,由于加密 LR 用的秘钥与切换后的秘钥不一致,autiasp 一定会发生校验错误。

关于这个情况,Linux 内核的应对有两点:

  • 在进程切换(context switch)的最后阶段才切换秘钥:

    1__switch_to(prev, next)
    2  ptrauth_thread_switch_user(next)
    3  last = cpu_switch_to(prev, next) // really the last moment
    4    ptrauth_keys_install_kernel
    

    其中 cpu_switch_to() 是一个汇编函数,切换了包括 SP 和 LR 在内的 callee-saved registers;ptrauth_keys_install_kernel 是一段汇编宏,不涉及函数上下文切换,仅切换 APIAKey。从内核线程的视角来看,进入和退出 cpu_switch_to() 时的 SP、LR 与 APIAKey 三者是匹配的。

  • 内核初始化秘钥的过程不涉及函数调用,如 commit 33e45234987e 所述:

    The keys for idle threads need to be set before calling any C functions, because it is not possible to enter and exit a function with different keys.

    1start_kernel             // never returns so it's fine
    2  boot_init_stack_canary // always_inline
    3    ptrauth_thread_init_kernel(current)   // macro
    4    ptrauth_thread_switch_kernel(current) // macro
    

其他厂商利用情况

目前已知苹果已在 A12 及后续 A 系列处理器上支持了 PAC,并在 iOS、macOS 等操作系统的层面上基于 PAC 实现了前向 CFI、后向 CFI 及 DFI 防护。摘自苹果开发者文档

  • Return addresses are signed with a key that’s unique per process, using a salt derived from the stack pointer.
  • Function pointers are signed with a key that’s fixed across all processes, allowing sharing of library code between processes.
  • Virtual method table entries are signed with a key that’s shared across all apps, using a salt derived from the method signature.

参考资料


  1. 应该认识到,指针的正确性与其所处的上下文有关。以返回地址为例,一个地址即便对某个函数来说是合法的返回地址,它也不应由其他函数使用。 ↩︎

  2. 该 config 由该补丁集 引入 v5.14-rc1 内核。 ↩︎

  3. SCTLR 中的 EnIA、EnIB、EnDA、EnDB(即 31、30、27、13)位控制 AP{I,D}{A,B}Key 的使能。详见《Arm® Architecture Reference Manual for A-profile architecture》 (发布号 I.a)中的 D17.2.118 SCTLR_EL1, System Control Register (EL1) 一节。 ↩︎

  4. ~~关于这里"kernel exit"的意思我有两种猜测,更倾向后者:若开启了内核态 PAC,理论上不需要也不应该在运行时关闭它。话说我在内核代码里没找到哪里有关闭 EnIA 的。~~通过阅读这个补丁 和 arch/arm64/kernel/entry.S,发现之前这里理解错了:kernel entry/exit 指的就是从用户态进入内核态/从内核态退出至用户态。 ↩︎


内核代码中的编译时检查
ARM64 虚拟地址长度判别