4. TEE 中断管理:物理中断与虚拟中断、中断路由配置 (GICv2/v3)、安全中断与非安全中断的隔离

中断管理,说白了就是TEE世界的“交通警察”。你想想看,一个物理CPU上同时跑着安全世界和普通世界,中断来了到底该谁处理?处理错了会怎样?我在早期一个项目中就踩过这个坑——安全中断被路由到了非安全世界,结果TEE的敏感操作直接被“扒光”了。嗯,今天我们就来彻底理清这件事。

4.1 物理中断 vs 虚拟中断:两个世界的“信号”

先搞清楚基本概念。物理中断是硬件实实在在拉起来的信号,比如UART收到数据、Timer超时。虚拟中断则是Hypervisor或TEE软件模拟出来的,专门喂给虚拟机或Guest OS的。

在TEE场景下,物理中断直接由GIC(Generic Interrupt Controller)分发。而虚拟中断,说白了就是TEE想让Normal World的某个VM“以为”自己收到了中断,实际上硬件根本没拉这根线。

核心区别一句话:物理中断是硬件直接干的,虚拟中断是软件“演”出来的。

我个人习惯把中断管理分成三层来看:

  • 硬件层:GICv2/v3负责物理中断的接收、优先级仲裁、路由分发。
  • 固件层:ATF(EL3)负责安全中断的接管和路由决策。
  • 软件层:TEE OS(EL1)处理安全中断,Normal World处理非安全中断。

这三层配合不好,系统就会出大问题。我记得有一次调试,发现安全中断在EL3被处理了两次——一次是ATF的同步异常,一次是TEE OS的IRQ。原因就是路由配置没搞清楚,中断被重复递交了。

4.2 GICv2 vs GICv3:中断路由的核心差异

GICv2和GICv3是当前最主流的两种中断控制器架构。很多工程师觉得它们差不多,其实在安全中断隔离上差别很大。

特性 GICv2 GICv3
中断路由方式 基于物理中断ID(INTID) 基于INTID + 安全组 + 流(Stream)
安全组支持 Group0(安全)、Group1(非安全) Group0(安全)、Group1(安全/非安全可配)
虚拟中断支持 有限,通过LR(List Register) 原生支持,通过ITS(Interrupt Translation Service)
中断优先级 8位,但实际只用了高4位 8位全可用,支持优先级分组
典型应用 ARMv7、早期ARMv8 ARMv8.2+、服务器、移动SoC

在GICv2中,安全中断必须配置为Group0,非安全中断为Group1。ATF在EL3收到Group0中断后,再决定是否转发给TEE。而GICv3引入了更灵活的Group1安全子组,允许某些中断在安全世界和非安全世界之间“共享”——当然,这需要你非常小心地配置。

注意:GICv3的Group1安全子组如果配置不当,非安全世界可能通过中断注入攻击绕过TEE的隔离。我曾经在审计一个客户代码时发现,他们把某个安全中断的Group1属性配成了“非安全”,结果Normal World可以直接触发TEE的敏感操作。这相当于把保险柜的钥匙挂在了门口。

4.3 安全中断与非安全中断的隔离:ATF的“守门员”角色

ATF在EL3扮演着中断路由的“守门员”。它的核心逻辑是这样的:

  1. CPU收到IRQ或FIQ异常,陷入EL3。
  2. ATF读取GIC的IAR(Interrupt Acknowledge Register),获取中断ID。
  3. 根据中断ID和安全组配置,判断该中断属于安全世界还是非安全世界。
  4. 如果是安全中断,ATF直接转发给TEE OS(通过SMC或直接异常返回)。
  5. 如果是非安全中断,ATF通过ERET返回到Normal World,让Rich OS处理。

这里有个关键点:ATF必须确保安全中断永远不会被Normal World看到。一旦泄露,攻击者就可以通过中断注入来破坏TEE的完整性。

我的经验:在配置GIC时,建议把所有的安全中断都设置为FIQ(快速中断),非安全中断设置为IRQ。这样ATF在EL3可以通过异常类型快速区分中断来源,减少路由延迟。我在一个车载项目中就是这么做的,中断响应时间从原来的2.5μs降到了1.2μs。

4.4 中断路由配置实战:GICv3示例

下面是一个典型的GICv3中断路由配置代码片段,用于将某个SPI(共享外设中断)配置为安全中断:

// 配置中断为Group0(安全)
gicd_set_igroupr(0, SPI_BASE + 42, 0);  // 0 = Group0

// 配置中断优先级(数值越小优先级越高)
gicd_set_ipriorityr(0, SPI_BASE + 42, 0x80);

// 配置中断目标(GICv3使用亲和性路由)
gicd_set_irouter(0, SPI_BASE + 42, 
                 MPIDR_TO_AFFINITY(0, 0, 0, 0));

// 使能中断
gicd_set_isenabler(0, SPI_BASE + 42, 1);

// 配置GICD的CTLR寄存器,启用Group0
gicd_write_ctlr(0, GICD_CTLR_ENABLE_GRP0);

这段代码看起来简单,但实际项目中容易出问题的地方在于:

  • 中断ID冲突:多个外设共享同一个SPI ID,导致路由混乱。
  • 优先级反转:安全中断优先级低于非安全中断,导致安全中断被“饿死”。
  • 亲和性错误:中断路由到了错误的CPU核心,导致TEE无法响应。

避坑指南:我曾经在一个项目中,把安全中断的优先级配成了0xFF(最低优先级),结果非安全中断频繁抢占CPU,安全中断几乎得不到处理。TEE的密钥生成操作超时了上百次。后来我强制规定:所有安全中断优先级必须高于0x7F,非安全中断必须低于0x80。这个“三八线”一直沿用至今。

4.5 虚拟中断的实现:TEE如何“欺骗”Normal World

虚拟中断是TEE实现隔离的重要手段。举个例子:TEE正在处理一个安全敏感操作,此时Normal World的某个外设产生了中断。TEE不想让Normal World知道自己在忙,于是它:

  1. 在EL3拦截这个非安全中断。
  2. 保存中断上下文。
  3. 向Normal World注入一个虚拟中断,告诉它“中断已经处理了”。
  4. 等TEE操作完成后,再恢复真实中断的处理。

在GICv3中,虚拟中断通过GICv3的LPI(Locality-specific Peripheral Interrupt)和ITS实现。TEE可以通过写GICR的寄存器,直接向某个vCPU注入中断。

// 向vCPU0注入一个虚拟中断(INTID=100)
gicr_write_sgi(0, 
               SGI_TARGET_LIST | 
               SGI_AFFINITY(0, 0, 0, 0) |
               SGI_INTID(100) |
               SGI_GROUP(1));  // 非安全组

这里要注意:虚拟中断的INTID不能和物理中断冲突,否则GIC会混淆。我建议在系统设计阶段就规划好物理中断和虚拟中断的地址空间,比如物理中断用0-1023,虚拟中断用1024-2047。

4.6 中断隔离的完整流程:一张图说清楚

下面这张SVG图展示了从物理中断产生到最终处理的完整流程,以及安全/非安全中断的隔离边界:

TEE中断隔离与路由完整流程 物理中断源 GICv3 分发器 安全组判断 安全中断 (Group0) ATF (EL3) 接管 TEE OS (EL1) 非安全中断 (Group1) Normal World (EL1) 虚拟中断注入 安全隔离边界 说明: 安全中断(绿色路径)必须经过ATF验证后进入TEE;非安全中断(红色路径)直接路由到Normal World。 虚拟中断注入用于TEE在安全操作期间“欺骗”Normal World,避免上下文泄露。

从这张图可以清楚看到:安全中断和非安全中断在GIC分发器之后就被严格分开了。ATF是安全中断的“必经之路”,而Normal World永远看不到安全中断的存在。这就是TEE中断隔离的核心思想。

4.7 常见问题与调试技巧

最后分享几个我在项目中遇到的典型问题:

  • 中断丢失:安全中断被GIC丢弃了。检查GICD的CTLR寄存器是否启用了Group0,以及中断是否被mask了。
  • 中断风暴:某个外设不断产生中断,导致TEE无法处理其他任务。建议在TEE OS中实现中断速率限制(rate limiting)。
  • 虚拟中断与物理中断冲突:虚拟中断的INTID和物理中断重叠。规划阶段就要分配好地址空间。

调试小技巧:在ATF中打印GIC的IAR和EOIR寄存器值,可以快速定位中断路由问题。我习惯在EL3的异常处理入口加一个条件断点,只捕获安全中断,这样调试日志不会太乱。

嗯,中断管理这块内容比较多,但核心就一句话:安全中断必须走ATF,非安全中断不能碰TEE。把这个原则刻在脑子里,配置GIC时就不会出大问题。