4. TEE内存隔离机制:TZASC、TZMA与安全非安全内存隔离

内存隔离,说白了就是给安全世界和非安全世界之间砌一堵墙。这堵墙砌得牢不牢,直接决定了TEE的根基稳不稳。我早年做第一块TEE芯片时,就因为在内存隔离上踩了个坑,导致安全应用的数据能被普通世界读出来——嗯,那场面,至今想起来后背发凉。

ARM TrustZone的内存隔离,主要靠两个硬件模块:TZASC和TZMA。它们分工不同,但目标一致——确保安全内存只有安全世界能碰,非安全世界连看都别想看。

核心原则:安全世界可以访问所有内存(安全+非安全),非安全世界只能访问非安全内存。这个单向可见性,是TEE内存模型的基础。

4.1 TZMA:简单粗暴的片上内存隔离

TZMA(TrustZone Memory Adapter)负责隔离片上SRAM。它做的事情很直接:把一块连续的SRAM从中间切开,前半段给安全世界,后半段给非安全世界。

你想想看,芯片内部的那点SRAM,通常也就几百KB。TZMA通过一个简单的大小寄存器来定义分界线。比如总共有256KB SRAM,你把寄存器设为128KB,那0x0000-0x7FFF就是安全区,0x8000-0xFFFF就是非安全区。

我个人的习惯:在项目初期,我会把TZMA的安全区设得大一些,比如总SRAM的75%。等安全应用稳定了,再慢慢调小,把更多资源让给非安全世界。别一开始就抠门,否则调试时你会发现安全堆栈溢出了,那才叫头疼。

TZMA的配置是静态的,通常在系统启动时由安全固件设置好,之后就不再改动。为什么?因为它的分界线是物理硬连线的,改一次就要重新初始化整个内存系统。

特性TZMA
管控对象片上SRAM
隔离粒度整块区域(通常KB级)
配置方式启动时静态配置
灵活性低,改分界线需要复位
典型大小几十KB到几MB

4.2 TZASC:灵活强大的片外内存隔离

TZASC(TrustZone Address Space Controller)就比TZMA灵活多了。它管控的是片外DRAM,也就是我们常说的DDR内存。DDR动辄几百MB甚至几GB,不可能像SRAM那样一刀切。

TZASC的核心思想是区域划分。它把DDR地址空间分成若干个区域,每个区域可以独立配置安全属性。我记得在某个项目中,我们用了8个区域,每个区域可以设置起始地址、结束地址、以及访问权限。

每个区域的配置项包括:

  • 基地址和大小:定义这个区域在物理内存中的位置
  • 安全属性:安全、非安全、或者安全/非安全都可访问
  • 可缓存性:是否允许CPU缓存
  • 执行权限:是否允许执行代码

我曾经踩过的坑:有一次配置TZASC时,我把安全世界的堆区域设成了"安全+可缓存",但忘了配置相应的缓存清理逻辑。结果安全世界和非安全世界之间切换时,缓存里的脏数据被写回了错误的内存区域。调试了整整三天,最后发现是缓存一致性的问题。所以记住:安全内存的缓存策略一定要和TEE的切换流程匹配。

4.3 TZASC的编程模型

TZASC的寄存器通常通过APB(高级外设总线)访问,而且这些寄存器本身也是安全敏感的——只有安全世界才能写它们。配置流程大致如下:

// 伪代码:配置TZASC区域
void tzasc_configure_region(uint32_t region_id, 
                            uint64_t base, 
                            uint64_t size, 
                            uint32_t attributes) {
    // 1. 先禁用该区域(防止配置过程中出现竞态)
    write_register(TZASC_REGION_CTRL(region_id), 0);
    
    // 2. 设置基地址(通常需要64位对齐)
    write_register(TZASC_REGION_BASE_LOW(region_id), base & 0xFFFFFFFF);
    write_register(TZASC_REGION_BASE_HIGH(region_id), (base >> 32) & 0xFFFFFFFF);
    
    // 3. 设置大小(必须是2的幂次,且对齐)
    write_register(TZASC_REGION_SIZE(region_id), size);
    
    // 4. 设置属性:安全/非安全、读/写、可执行等
    write_register(TZASC_REGION_ATTR(region_id), attributes);
    
    // 5. 使能该区域
    write_register(TZASC_REGION_CTRL(region_id), 1);
}

这里有个细节:TZASC的区域大小必须是2的幂次,而且基地址必须对齐到大小边界。比如你要配置一个4MB的安全区域,基地址必须是4MB的整数倍。这个限制在早期规划内存布局时就要考虑进去。

4.4 安全内存与非安全内存的交互

隔离归隔离,两个世界总得交换数据吧?比如安全应用要解密一个文件,然后把明文传给普通世界。这时候就需要共享内存机制。

共享内存的实现方式有两种:

  1. 非安全缓冲区:安全世界主动把数据写到非安全内存区域,然后通知非安全世界来取。这种方式安全世界需要做一次内存拷贝。
  2. 安全缓冲区:在TZASC中配置一个"安全+非安全可读"的区域。双方都能访问,但只有安全世界能写。这种方式减少了拷贝,但配置要格外小心。

我建议:除非性能要求极高,否则优先用非安全缓冲区方案。安全缓冲区虽然省了一次拷贝,但配置不当很容易变成安全漏洞——万一非安全世界找到了写这个区域的方法,那安全数据就全暴露了。

4.5 内存隔离的完整视图

下面这张图展示了TZASC和TZMA在整个系统中的位置和协作关系:

TEE内存隔离架构图 CPU Core 安全/非安全模式 系统总线(AXI/AHB) TZMA 片上SRAM隔离 TZASC 片外DRAM隔离 安全内存 安全SRAM + 安全DRAM区域 仅安全世界可访问 非安全内存 非安全SRAM + 非安全DRAM区域 两个世界都可访问 共享内存区域 安全写 + 非安全读 关键点 • TZMA:片上SRAM一刀切 • TZASC:片外DRAM分区 • 安全→非安全:单向可见 • 共享内存:需显式配置

4.6 实际项目中的避坑指南

做TEE内存隔离,有几个地方特别容易出问题:

  • DMA绕过问题:有些外设支持DMA(直接内存访问),它们可以直接读写内存,不经过CPU的MMU。如果DMA控制器没有被纳入TZASC的保护范围,它就能绕过安全隔离。我见过一个案例,某个网卡DMA直接把数据写到了安全内存区域,导致安全密钥泄露。
  • 大小核架构:现在的SoC通常有多个CPU簇,每个簇有自己的L2缓存。如果不同簇的缓存策略不一致,安全世界在一个簇上运行,非安全世界在另一个簇上运行,缓存一致性协议可能会把安全数据同步到非安全缓存中。
  • 休眠唤醒:系统进入深度休眠后,TZASC的寄存器状态可能会丢失。唤醒后如果恢复顺序不对,安全内存会在短暂时间内处于未保护状态。我建议在休眠前保存TZASC的配置快照,唤醒后第一时间恢复。

特别提醒:永远不要假设非安全世界的软件是"友好"的。攻击者可能已经控制了整个非安全操作系统。你的TZASC配置必须假设非安全世界会尝试所有非法访问——包括越界读、越界写、以及尝试修改TZASC寄存器本身。

嗯,内存隔离这块,说复杂也复杂,说简单也简单。核心就是记住:TZMA管片上,TZASC管片外,两者配合把安全世界和非安全世界的内存彻底隔开。配置的时候多想想边界情况,多想想攻击面,基本上就不会出大问题。