文章摘要
Fil-C 0.680版本起支持ucontext和setjmp/longjmp等上下文切换API,并确保内存安全。这些API常用于异常处理、协程和纤程实现,但容易因误用导致栈损坏。Fil-C通过特殊实现防止栈损坏和能力模型违规。
文章总结
好的,这是对原文主要内容的中文重述,保留了关键细节,并删减了与主题无关的冗余内容。
标题:内存安全的上下文切换
核心内容: 本文档阐述了 Fil-C 如何在完全内存安全的前提下,支持 longjmp、setjmp 以及 ucontext 系列 API(setcontext、getcontext、makecontext、swapcontext)。在 Fil-C 中,对这些 API 的任何误用都不会导致栈损坏或违反其能力模型。
背景与挑战:
longjmp/setjmp常用于 C 语言中的异常处理,尤其是在信号处理程序中。ucontextAPI 用于实现协程和纤程。- 实现这些 API 的内存安全版本非常困难,因为误用可能导致恢复一个悬空(dangling)的栈。例如:
- 在函数中保存上下文后,该函数返回或线程退出,再尝试恢复该上下文。
- 使用
makecontext创建一个指向某栈的上下文,然后释放该栈,再切换到该上下文。 - 在
swapcontext中错误地将当前执行的上下文作为第二个参数传入。
在传统的 C 语言(Yolo-C)中,这些误用会导致难以调试的崩溃,甚至被攻击者利用。在 Fil-C 中,所有此类情况要么在误用 API 时触发程序恐慌(panic),要么由于 Fil-C 的栈管理机制而成为合法的执行。
setjmp/longjmp 的内存安全实现:
setjmp的“邪恶”本质:setjmp会“返回两次”,这给编译器优化带来了巨大挑战。编译器必须知道setjmp会返回两次,才能避免错误地复用栈上变量的溢出槽(spill slot),否则在longjmp后,变量的值可能不符合预期。文章通过一个复杂的示例展示了即使变量没有被常量折叠或寄存器分配,也可能因为溢出槽复用而打印出错误的值。Fil-C 的解决方案:
- 不透明对象:
jmp_buf内部仅包含一个指向不透明对象zjmp_buf的指针。Fil-C 代码无法直接访问其内容,只能通过运行时库操作。 - 强制直接调用:只能通过直接调用
setjmp符号来使用它,任何间接调用(如通过函数指针)都会导致编译器内部错误(ICE),从而确保编译器总能识别setjmp并正确禁用溢出槽复用。 - 栈帧关联:
setjmp调用会分配一个新的zjmp_buf对象,并将其与当前栈帧关联。每个栈帧都维护一个“弱集合”,记录哪些zjmp_buf是有效的跳转目标。 - 祖先检查:
longjmp只有在被调用的栈帧是某个认为该zjmp_buf有效的栈帧的祖先时,才会成功执行,否则会触发恐慌。这通过遍历调用栈并检查每个栈帧的“弱集合”来实现。 - GC 根保存:
zjmp_buf会保存调用setjmp时栈帧的 GC 根(垃圾回收根)的副本,确保在zjmp_buf存活期间,这些根能被正确标记。
- 不透明对象:
ucontext 的内存安全实现:
核心思路:Fil-C 对
ucontextAPI 的使用施加了比严格必要更保守的规则,以确保安全。安全规则:
- 不透明状态:
ucontext_t内部包含一个指向不透明对象zfiber_context的指针。 - 忽略用户栈:实现完全忽略用户提供的
ss_sp栈指针。zfiber_context会在内部根据ss_size分配一个不可见的栈。 - 受限状态机:
zfiber_context有严格的状态转换:- 未初始化:只能调用
getcontext或作为swapcontext的“来源”(from)参数。 - 已获取上下文:只能调用
makecontext或作为swapcontext的“来源”参数。 - 可运行:只能调用
setcontext或作为swapcontext的“目标”(to)参数。 - 运行中:只能作为当前线程正在运行的上下文,并作为
swapcontext的“来源”参数。
- 未初始化:只能调用
- 线程亲和性:
zfiber_context会记录其创建线程,并禁止从其他线程调用其任何 API。
- 不透明状态:
GC 集成:
- 当 GC 标记一个“可运行”状态的
zfiber_context时,会扫描其栈上的指针。 - 当通过
swapcontext从一个“运行中”的上下文切换出去,使其变为“可运行”时,如果 GC 标记阶段尚未结束,该上下文会被加入当前线程的“灰色纤程”列表,以确保 GC 在终止前会重新扫描其栈。 - 文章解释了在并发 GC 的软握手机制下,这种设计如何避免竞态条件,并论证了其正确性。
- 当 GC 标记一个“可运行”状态的
总结:
Fil-C 通过一系列精巧的设计,成功地在内存安全的前提下支持了 longjmp/setjmp 和 ucontext 这两种上下文切换机制。ucontext 支持是 0.680 版本后的新功能,需要从源码构建。longjmp/setjmp 的实现则更为成熟。这表明,即使是最复杂、最容易被滥用的 C 语言特性,也能在保证内存安全的前提下得到支持。
评论总结
根据评论内容,主要围绕Fil-C对setjmp/longjmp及ucontext等非局部跳转机制的内存安全处理展开讨论,观点存在分歧。以下是总结:
观点一:setjmp/longjmp存在固有风险,Fil-C的改进有价值
- 评论2指出,使用setjmp/longjmp的代码风险远大于单纯的内存安全问题,但若必须使用,应尽力缓解风险。
- 关键引用:"code that uses setjmp/longjmp often has a risk profile that's way bigger than memory safety alone. If you're stuck with them then by all means, mitigate all you can."
- 评论4认为,管理栈本质也是管理内存,Fil-C在此有贡献,并赞赏文章对setjmp/longjmp与寄存器分配、栈溢出交互的复杂性的具体分析。
- 关键引用:"I suppose managing the stack is still managing memory after all... so Fil-C has something to add here." "It's really worth reading the section here about the complexity of setjmp/longjmp... going in to the specifics is delicious."
观点二:setjmp/longjmp与内存安全无关,风险来自信号处理
- 评论3反驳,认为longjmp、setjmp等与安全性(包括内存安全)无关,真正需要处理的是sigaction(2)及上下文切换驱动方式。
- 关键引用:"longjmp, setjmp... have no bearing on safety, memory or otherwise. What you have to deal with is what is represented by sigaction(2)..."
观点三:ucontext效率低,现代实现更优
- 评论5指出,Boost的fiber实现使用汇编级ABI支持,开销接近虚函数调用,而ucontext非常重。作者自己用汇编实现了更高效的fiber库。
- 关键引用:"Boost context and Boost fiber has ABI support... The overhead for a fiber switch... is about as heavy as a virtual function call. In comparison, ucontext is very heavy."
观点四:setjmp/longjmp的局限性可通过复制栈帧解决,但C语言指针阻碍
- 评论6认为,setjmp/longjmp的栈帧有效性限制可通过复制栈帧并整体覆盖当前栈来解除(类似分隔延续),但C语言中指向栈的指针使栈不可自由重定位。
- 关键引用:"That limitation could be lifted by simply copying the stack frames somewhere else... This is how delimited continuations work! What ruins this for C is the existence of pointers."
观点五:对术语的澄清
- 评论7质疑文章中的“祖先”表述,认为应为“后代”(descendent),即longjmp应从setjmp的调用链后代帧中调用。
- 关键引用:"I think you mean descendent and not ancestor here?"