Fil-C 内存安全上下文切换
Fil-C 内存安全上下文切换
Fil-C 提供了 setjmp、longjmp 和 ucontext API(getcontext、setcontext、makecontext 和 swapcontext)的内存安全实现。这些机制确保了对上下文切换 API 的误用不会导致栈损坏或违反 Fil-C 的能力模型,特别是防止了悬空栈的恢复。
内存安全的 setjmp 和 longjmp
Fil-C 通过将跳转缓冲区视为不透明对象并强制执行严格的栈帧验证,来防止 setjmp 和 longjmp 中常见的内存安全陷阱。
标准 setjmp/longjmp 的危险性
在标准 C 中,setjmp 保存了寄存器状态(被调用者保存寄存器、栈指针和指令指针),但没有保存栈本身。这带来了两个主要的安全性风险:
- 悬空栈: 如果程序调用了
setjmp然后从该函数返回,保存的上下文就会变得无效。随后对该上下文进行longjmp会尝试恢复一个不再存在的栈帧,导致机器状态撕裂。 - 编译器优化错误: 由于
setjmp可能返回两次,编译器必须为调用它的函数禁用某些优化(例如溢出槽重用)。如果编译器不知道一个函数正在调用setjmp(例如通过函数指针),它可能会重用溢出槽,导致变量在longjmp之后持有错误或垃圾值。
Fil-C 的实现策略
Fil-C 通过以下机制减轻这些风险:
- 不透明跳转缓冲区:
jmp_buf包含一个指向由 Fil-C 运行时完全管理的、不透明的zjmp_buf对象的指针。这防止了用户伪造或手动操纵跳转状态。 - 编译器强制执行: Fil-C 编译器 (
filcc) 要求必须直接调用setjmp。任何试图混淆调用的尝试(例如使用函数指针)都会导致编译器错误或内部编译器错误 (ICE),从而确保编译器的returns_twice逻辑始终被触发。 - 栈帧注册: 当调用
setjmp时,它会分配一个zjmp_buf并将其注册到当前的栈帧。每个栈帧都维护一个包含有效zjmp_buf目标的弱集合。 - 祖先验证: 除非当前栈帧是创建
zjmp_buf的栈帧的后代(即目标栈帧是当前调用栈上的祖先),否则longjmp将会触发 panic。这是通过遍历栈并检查该栈帧的有效集合中是否存在zjmp_buf来验证的。 - GC 集成:
zjmp_buf在setjmp调用时存储了该栈帧在当时 GC 根部的副本。只要zjmp_buf处于活跃状态,GC 就会继续标记这些根部。
内存安全的 ucontext API
Fil-C 在 0.680 版本中引入了对 ucontext API 的支持。这些 API 用于通过允许保存和恢复执行上下文来实现 fiber 和协程。
ucontext 状态机
为了防止误用,Fil-C 为 zfiber_context 对象实现了一个受限的状态机:
- Uninitialized (未初始化): 初始状态。仅允许使用
getcontext或作为swapcontext的from参数。 - After_getcontext (getcontext 之后):
getcontext返回后的状态。仅允许使用makecontext或作为swapcontext的from参数。 - Runnable (可运行):
makecontext之后或作为swapcontext的from参数后的状态。仅允许使用setcontext或作为swapcontext的to参数。 - Running (运行中): 上下文当前正在执行的状态。仅允许使用
swapcontext,且仅当该上下文是当前在线程上运行的上下文时。
安全约束与实现
- 栈管理: Fil-C 忽略
ucontext_t中用户提供的ss_sp(栈指针)。相反,运行时会在makecontext期间在内部分配一个受管理的栈,以确保该栈与 Fil-C 的溢出处理机制兼容。 - 线程亲和性: 为了保持 ABI 一致性,
zfiber_context会跟踪其创建时所在的线程。它禁止切换到来自不同线程的上下文,从而防止栈在线程之间迁移。 - 不透明状态: 与
jmp_buf一样,ucontext_t包含一个指向不透明的zfiber_context对象的指针,从而向用户隐藏了内部机器状态。
垃圾回收与 Fiber 切换
将 fiber 与即时垃圾回收器集成需要仔细跟踪那些目前不属于某个线程的栈。
灰色 Fiber 问题
当一个 fiber 处于 runnable 状态(当前未在执行)时,其栈必须由 GC 进行扫描。然而,如果一个 mutator 在 GC 标记阶段切换到了一个 fiber,然后又在标记期间切换走了,如果 GC 已经将该 fiber 标记为 "black" (已处理),GC 可能会错过该 fiber 的栈。
Fil-C 通过跟踪 灰色 fiber 来解决这个问题。当在标记阶段调用 swapcontext 时,正在被切换走的 (from) 上下文会被添加到当前线程的 grey_fibers 列表中。在 GC 终止前的最后一次栈重扫描期间,线程会遍历其列表中所有灰色 fiber 的栈,以确保不会错过任何活跃对象。
API 支持摘要
| API | Fil-C 安全机制 | 主要用例 |
|---|---|---|
setjmp / longjmp |
祖先验证 & 不透明 zjmp_buf |
异常处理, 信号处理程序 |
getcontext / makecontext |
状态机 & 受管理栈 | Fiber/协程引导 |
swapcontext |
线程亲和性 & 灰色 fiber GC 跟踪 | Fiber 上下文切换 |
| } |