Ringmaster: How to juggle high-throughput host OS system calls from TrustZone TEEs¶
个人认为这是一篇相当好的文章, 值得精读~
TLDR¶
如何在 TrustZone TEE 中高效处理宿主操作系统的高吞吐量系统调用
(1) 研究背景与核心挑战
-
背景:
- 现代信息物理系统(CPS,如自动驾驶汽车、无人机、手术机器人)既需要处理对时间敏感的传感器输入以确保安全
- 又越来越依赖功能丰富的操作系统(如 Linux)来支持人工智能、视频流等复杂应用
-
安全威胁:
- 丰富的操作系统代码庞大,容易存在安全漏洞
- 如果攻击者获得了操作系统的最高权限,他们可以通过拒绝为时间敏感的程序提供服务来造成现实世界中的物理损害
-
现有方案的局限性:
- 依赖丰富 OS 的 TEE (可信执行环境):
- 完全依赖不受信任的 OS 进行调度和内存管理,极易遭受时间或拒绝服务(DoS)攻击(例如: OS 恶意延迟或阻塞同步系统调用)
- 强调可用性的 TEE:
- 通常缺乏丰富的 OS 服务(如网络栈、文件系统),开发困难,且难以运行现有的大型应用程序
- 依赖丰富 OS 的 TEE (可信执行环境):
(2) Ringmaster 解决方案核心
Ringmaster 是一个新颖的框架,它允许 TrustZone 中的 enclave(飞地/可信执行环境)通过 Linux 的 io_uring 机制,以异步的方式安全地访问丰富但可能不受信任的 OS 服务
- 核心机制:
- 通过将
io_uring的提交队列(SQ)和完成队列(CQ)映射到 enclave 的地址空间中,使 enclave 能够发出无阻塞的 I/O 系统调用
- 通过将
- 安全保障:
- 如果底层 Linux OS 发生崩溃或被恶意控制而拒绝服务,enclave 仍可以在 Ringmaster 提供的极简 ARM TrustZone 内核上继续运行
- 并直接访问关键的底层设备驱动(如 UART、I2C)以保证基本生命线和物理安全
(3) 关键技术贡献
-
跨信任边界的异步系统调用:
- 设计了一种零拷贝机制,用于在 enclave 和代理进程之间传递大型参数
- 解决了跨地址空间的
io_uring队列安全映射问题
-
动态共享内存管理(Arenas):
- 针对不受信任的宿主 OS 可能破坏内存的风险,设计了基于 Arena 的高效共享内存管理器,提供安全的参数传递
-
Promises 异步编程框架:
- 为 C 语言开发者提供了一套安全的异步编程接口,以防止恶意 OS 导致的调用链无限阻塞或内存耗尽
-
向后兼容(Ringmaster LibC):
- 基于 Musl LibC 构建了自定义的 C 标准库:
- 允许绝大多数未修改的传统 POSIX 应用程序直接在 TrustZone 中运行
- 并通过超时和信号机制防止饥饿
(4) 实验与评估结果
-
吞吐量与性能:
- 在 Raspberry Pi 4B 上的无人机实验中,Ringmaster 实现了近 1 GiB/sec 的高吞吐量顺序文件系统读写
- 与非 enclave 的正常任务相比,吞吐量开销仅为 0-3%
- 网络 I/O 能够完全跑满千兆以太网端口
-
延迟:
- 由于避免了传统的同步系统调用和环境切换开销,Ringmaster 的 I/O 延迟表现优于或相当于以往的 TEE 系统调用方案
-
安全性验证(物理控制实验):
- 倒立摆实验:即使 Linux 端的标准输出被攻击者恶意节流阻塞,运行在 Ringmaster enclave 中的 PID 控制循环依然保持 100Hz 的稳定运行,倒立摆未倒下
- 无人机避障实验:即使宿主 OS 被完全攻破或瘫痪,运行在 enclave 中的路径规划器依然能依靠直接读取 LiDAR 传感器成功避开障碍物,防止坠机
核心机制解读¶
(1) 跨信任边界的异步系统调用: 零拷贝与 io_uring 映射¶
-
原来的问题:
- 传统的系统调用(比如读写文件)是“同步”的,也就是程序发出请求后,必须停下来死等 OS 处理完并返回结果
- 如果系统内核是恶意的,它只要故意不返回结果,就能把这个关键程序永远“卡死”
- 同时,由于 Enclave(飞地)和普通操作系统的内存是完全隔离的(信任边界),普通指针无法直接传递数据
-
Ringmaster 的解法:
- 借用 io_uring:Linux 有一个叫
io_uring的机制 - 它通过两个队列通信:提交队列(SQ,放请求)和完成队列(CQ,拿结果)
- Ringmaster 直接把这两个队列的内存映射到了 Enclave 的安全内存空间里
- 借用 io_uring:Linux 有一个叫
-
异步且不卡死:
- Enclave 只需要把请求丢进队列就可以继续干别的活,完全不需要陷入(trap)内核去死等
- 恶意 OS 也就无法通过阻塞调用来卡死程序了
-
零拷贝与指针翻译:
- 当需要传递大文件或长字符串时,Ringmaster 引入了一个叫
IORING_OP_ENCLAVE_MMAP的新操作来向 OS 动态申请共享内存 - 申请到之后,Ringmaster 的底层库会自动把你代码里的内存指针翻译成 Linux 听得懂的地址
- 不需要在内外环境之间来回复制数据(即: 零拷贝),极大提高了吞吐量
- 当需要传递大文件或长字符串时,Ringmaster 引入了一个叫
队列内存的安全映射机制
-
代理进程协助:
- 每个 Enclave 都有一个在 Linux 中运行的代理进程
- 该代理负责创建
io_uring队列,并提交一个名为IORING_OP_ENCLAVE_SPAWN的特殊条目
-
物理地址注册:
- Linux 侧的 Ringmaster 内核模块接收到该请求后,会将队列的物理内存地址注册到底层的可信操作系统(Ringmaster OS)中
-
隔离映射:
- Enclave 会通过同步系统调用向 Ringmaster OS 检查已注册的内存
- 随后 Ringmaster OS 负责管理 Enclave 的页表,将提交队列(SQ)和完成队列(CQ)的内存安全地映射到 Enclave 的专属地址空间内
(2) 动态共享内存管理¶
-
原来的问题:
- 既然需要共享内存来传递系统调用的参数,那就得有内存分配机制(类似 C 语言的
malloc/free) - 但是,普通的
malloc往往会把“这块内存有多大、状态是什么”这些元数据(Metadata)也存在共享内存里 - 恶意 OS 可以轻易篡改这些元数据,导致 Enclave 发生内存损坏或被攻击
- 既然需要共享内存来传递系统调用的参数,那就得有内存分配机制(类似 C 语言的
-
Ringmaster 的解法(Arena):
- 生命周期绑定:
- 系统调用往往是一连串的(比如打开文件、读文件、关文件),参数的生命周期很相似
- Ringmaster 设计了 Arena(一种大块的连续内存池)来专门应对这种场景
- 安全且极速的分配:
- 在 Arena 中分配和释放内存就像操作栈一样(Push 压入,Pop 弹出),速度极快(常数时间)
- 更重要的是: 开发者操作的是 "句柄" 而不是 "裸指针",所有的元数据都被安全地藏在了 Enclave 的私有内存里,OS 绝对摸不到也改不了
- 生命周期绑定:
handle 与 metadata 隔离
-
传统指针的风险:
- 传统的内存分配器在分配共享内存时,通常会将分配相关的元数据(如内存块长度、偏移量或空闲链表指针)直接存在共享内存本身中
- 这意味着: Malicious HostOS 可以直接覆盖或篡改这些数据来发起攻击
-
句柄的抽象层:
- 为了解决这个问题,Ringmaster 让开发者主要管理显式的共享内存“句柄”(Handles),而不是直接操作裸指针
-
私有化存储:
- 在这个基于 Arena 的系统中,Arena 的元数据结构体完全归程序员所有,且被安全地保存在 Enclave 内部的私有内存中
- 使用句柄不仅能让开发者清晰地界定哪些内存是公开共享的,还能实现安全的元数据快速查找
(3) Promises 异步编程框架¶
-
原来的问题:
- 虽然底层实现了异步,但对于用 C 语言写代码的程序员来说,管理一堆“发出去还没回来”的异步事件极其痛苦,很容易写出无限循环等待的 Bug
- 此外: 如果恶意 OS 故意拖延不给回复,程序会在内存里堆积大量的等待事件,最终把内存撑爆("内存 DoS 攻击")
-
Ringmaster 的解法:
- 引入前端理念:
- Ringmaster 为 C 语言引入了类似 JavaScript 中的
Promise概念 - 当你发起一个写文件请求时,函数会立刻返回一个 Promise 对象,代表一个 “未来的承诺”
- Ringmaster 为 C 语言引入了类似 JavaScript 中的
- 链式调用与安全上限:
- 程序员可以通过这个对象把一系列操作链式连接起来,只需要去轮询这个 Promise 的状态即可,代码非常清晰
- 同时,为了防止恶意 OS 故意积压未完成的 Promise 来耗尽内存,Ringmaster 强制设置了 Promise 调用栈的静态上限,保证了系统的可用性
- 引入前端理念:
Promise 的链式连接机制
-
内部数据结构:
- Ringmaster 的
Promise对象在内部存储了:- 一个函数指针
- 一个指向下一个操作的
next指针 - 一个固定大小的参数数组
- Ringmaster 的
-
异步调用栈:
- 通过这个
next指针,多个单一的 Promise 被互相链接起来 - 这种链式结构在实际运行时的表现相当于一个 "asynchronous call stack"
- 通过这个
-
复杂操作分解:
- 以写文件函数
ringmaster_write为例,它在内部:- 首先: 会请求一块空闲的共享内存并生成一个
Promise - 接着: 调用
ringmaster_sqe去获取队列资源并生成下一个 Promise - 该机制将“等待排队”、“写入数据”等环节链式绑定,最终向上层返回 整个操作链的末端 Promise
- 首先: 会请求一块空闲的共享内存并生成一个
- 极大简化了异步管理
- 以写文件函数
(4) 向后兼容: Ringmaster LibC¶
-
原来的问题:
- 很多安全方案虽然好,但要求开发者把原来的代码全部重写一遍,去适配新的安全 API,这在现实工业界是很难推进的
-
Ringmaster 的解法:
- 瞒天过海的 C 标准库:
- Ringmaster 基于 Musl LibC 深度魔改了一套自己的 C 标准库
- 对于上层的老程序(比如传统的 POSIX 软件)来说,它们以为自己还在调用普通的同步
read()或write()
- 底层暗度陈仓:
- 实际上: 这个特殊的 LibC 会把这些标准的同步调用,在底层偷偷转换成上述的安全
io_uring异步操作 - 因此,绝大多数未修改的旧程序可以直接编译并在 TrustZone 中跑起来
- 实际上: 这个特殊的 LibC 会把这些标准的同步调用,在底层偷偷转换成上述的安全
- 瞒天过海的 C 标准库:
-
防饥饿机制:
- 为了防止老程序在等待模拟的同步结果时被恶意 OS “饿死”(无限期拖延),Ringmaster 的 LibC 允许开发者对系统调用设置具体的 Timeout
- 或: 注册
SIGALRM信号来强行打断死循环,保证程序永远能拿回控制权