返回

文章详情

Fil-C中的内存安全上下文切换(longjmp, setjmp)

Hacker News2026年6月30日 00:38

自版本0.680以来,支持ucontext API是新的。如果你想玩setcontext、getcontext、makecontext和swapcontext,你需要从源代码构建。本文档描述了Fil-C如何以完全内存安全的方式支持longjmp、setjmp、setcontext、getcontext、makecontext和swapcontext。特别是,在Fil-C中对这些API的任何误用都不会导致堆栈损坏或其他任何对Fil-C能力模型的侵犯。这些API被广泛使用:longjmp和setjmp被用于C程序中实现异常处理。尤其常见的是在信号处理程序中实现“抛出”的异常。getcontext、setcontext、makecontext和swapcontext(即ucontext API)被用于实现协程和纤程。例如,Boost作为其纤程实现的一部分使用ucontext。ucontext API的使用不如longjmp/setjmp常见,并且一些操作系统(如Darwin)已将其弃用。然而,它们在glibc中仍然得到了很好的支持。以保持内存安全的方式实现这些API是困难的,因为它们的误用可能导致恢复悬空的堆栈。例如,你可以在某个函数内调用setjmp或getcontext,然后执行以下任一操作:从该函数返回。在这一点上,保存的上下文将尝试恢复这样的堆栈框架,它已不再存在。从线程退出。在这一点上,保存的上下文将尝试在已经释放的堆栈上恢复执行。甚至更友好的API,如makecontext和swapcontext,也可能被简单地误用:你可以使用makecontext创建一个指向某个堆栈的上下文,然后释放该堆栈,再使用swapcontext或setcontext来切换到该上下文。在Yolo-C中,这会导致在悬空堆栈上执行。而在Fil-C中,这不会出错。你可以调用swapcontext,并将第二个参数设置为当前正在执行的上下文。这可能发生在你混淆了第一个和第二个参数的情况下。在Yolo-C中,在最佳情况下,这表现得像longjmp;在最坏的情况下,这将在悬空堆栈上执行。而在Fil-C中,这是一个安全错误,会导致你的程序崩溃。在Yolo-C中,悬空堆栈上的执行会导致最混乱的崩溃,因为调试器甚至无法打印堆栈跟踪!更糟的是,如果程序在处理上下文时有微妙的错误,那么攻击者可以利用这些错误让程序执行攻击者想要的事情。在Fil-C中,在悬空堆栈上执行是不可能的:所有这些情况要么是在误用longjmp或某个ucontext API时发生的崩溃,要么是因为Fil-C管理堆栈的方式而可靠合法的执行。Fil-C以不同的方式实现setjmp/longjmp和ucontext API。 使setjmp/longjmp内存安全 有关setjmp的堕落性深度令人印象深刻。在详细讨论Fil-C如何实现setjmp/longjmp之前,我们需要讨论究竟是什么让这个函数如此邪恶。setjmp在调用时保存上下文,以便在后续调用longjmp时,setjmp将再次返回。正是它会返回两次的事实使其如此可怕,因此我们需要准确理解其含义。 一个例子考虑这个简单的程序: #include <setjmp.h> #include <stdio.h> int main(int argc, char** argv) { volatile int x = 42; jmp_buf jb; if (setjmp(jb)) { printf("x = %d\n", x); return 0; } x = 666; longjmp(jb, 1); printf("不应该到这里。\n"); return 1; } 这个程序打印:x = 666 然后退出。流程是:第一次调用setjmp时,它返回0并将其调用者的上下文保存到jb中。然后我们将x设置为666,并用值1的longjmp跳转到jb。setjmp返回1,因此我们打印并退出。请注意,我们必须将x标记为volatile,以便程序能够可靠地打印666。否则,编译器可以优化对x的访问,使其返回42。这可能以以下几种方式发生: 编译器可能将x常量折叠为42。如果我们删除volatile并使用任何优化级别高于-O0,这将在示例中发生。然后会打印x = 42。 假设常量折叠没有发生,可能是因为我们在x的定义后插入了asm("" : "+r"(x))。在这种情况下,编译器可能会将x分配到一个调用者保存的寄存器中,在这种情况下,该寄存器最后被setjmp保存。这也会导致打印x = 42。 假设由于某种原因我们遇到寄存器压力,并且x没有进入调用者保存的寄存器,而是被溢出。在优化级别高于-O0时,编译器会将x分为两个变量:一个用于x = 42,另一个用于x = 666,而printf将引用第一个变量(因为x = 42主导了printf)。这两个变量几乎总是会得到单独的溢出槽。因此,当我们第二次退出setjmp时,读取x仍然会得到42。

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡