Fil-C 内存安全上下文切换

Fil-C 内存安全上下文切换

Fil-C 提供了 setjmplongjmpucontext API(getcontextsetcontextmakecontextswapcontext)的内存安全实现。这些机制确保了对上下文切换 API 的误用不会导致栈损坏或违反 Fil-C 的能力模型,特别是防止了悬空栈的恢复。

内存安全的 setjmp 和 longjmp

Fil-C 通过将跳转缓冲区视为不透明对象并强制执行严格的栈帧验证,来防止 setjmplongjmp 中常见的内存安全陷阱。

标准 setjmp/longjmp 的危险性

在标准 C 中,setjmp 保存了寄存器状态(被调用者保存寄存器、栈指针和指令指针),但没有保存栈本身。这带来了两个主要的安全性风险:

  1. 悬空栈: 如果程序调用了 setjmp 然后从该函数返回,保存的上下文就会变得无效。随后对该上下文进行 longjmp 会尝试恢复一个不再存在的栈帧,导致机器状态撕裂。
  2. 编译器优化错误: 由于 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_bufsetjmp 调用时存储了该栈帧在当时 GC 根部的副本。只要 zjmp_buf 处于活跃状态,GC 就会继续标记这些根部。

内存安全的 ucontext API

Fil-C 在 0.680 版本中引入了对 ucontext API 的支持。这些 API 用于通过允许保存和恢复执行上下文来实现 fiber 和协程。

ucontext 状态机

为了防止误用,Fil-C 为 zfiber_context 对象实现了一个受限的状态机:

  • Uninitialized (未初始化): 初始状态。仅允许使用 getcontext 或作为 swapcontextfrom 参数。
  • After_getcontext (getcontext 之后): getcontext 返回后的状态。仅允许使用 makecontext 或作为 swapcontextfrom 参数。
  • Runnable (可运行): makecontext 之后或作为 swapcontextfrom 参数后的状态。仅允许使用 setcontext 或作为 swapcontextto 参数。
  • 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 上下文切换
}

Sources