FFI in Miri at 8000 segfaults per second

Lobsters Hottest 工具

摘要

Nia Deckers 在 RustWeek 上介绍了一种利用 ptrace 和 SIGSEGV 为 Miri 实现 FFI 执行的新方案,通过 fork 进程、设置内存保护、反汇编指令来确定内存访问细节,并结合互斥锁解决竞态问题。该方法能以“每秒8000个段错误”的代价让 Miri 追踪任意外部函数调用,并意外提供了调试器功能。

<p>Talk by Nia Deckers at RustWeek</p> <p><a href="https://lobste.rs/s/daehjf/ffi_miri_at_8000_segfaults_per_second">Comments</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/15 00:50

**TL;DR:** Nia Deckers通过ptrace和SIGSEGV为Miri实现了一款"每秒8000个段错误"的FFI执行方案——fork进程、用PROT_NONE触发段错误、反汇编确定访问大小、再劫持指令指针跳转到假函数,同时使用互斥锁解决竞态,并借助X speaks反汇编器改善跨架构支持。 ## 背景:Miri的FFI困境 Miri是Rust的MIR解释器,能精确追踪指针出处(provenance)和内存初始化状态。但当调用F​​FI(外部函数接口)时,目标代码可能是预编译的C/C++甚至Java库——Miri完全不知道它做了什么内存操作。 > “你基本只能硬着头皮编造结果。” 不可能追踪所有细节,尤其当链接一个任意的`.so`文件时。但可以想出一个“最坏情况”的假设,比之前“直接假设它可能对内存做任何事”要好得多。这意味着需要追踪每一次内存访问:地址、大小、读写方向。 ## 方案:Ptrace + 段错误 “我在东欧长大,可能比你们中很多人更保守。所以我第一个想到的是去翻阅神圣的经文——谷歌搜索结果和Stack Overflow。” 她找到了一个十年前的帖子:用SIGSEGV。 **核心思想**: 1. 把Miri fork成两个进程。 2. “监控进程”(supervisor)负责处理段错误;另一个进程就是Miri本身,跳进FFI。 3. 在Miri进入FFI前,把目标内存区域设为`PROT_NONE`。任何访问都会触发SIGSEGV。 4. 监控进程通过ptrace捕获段错误,并获取导致错误的地址。 5. 但还需要知道访问的大小、读写方向。怎么办?直接读取另一个进程的指令指针处的数据,然后塞进一个反汇编器。 ### 反汇编确定访问细节 “我们是Rust项目团队。我们热爱解析、验证之类的。” 她用了Capstone反汇编器。 - 读取指令指针处的字节码,反汇编后得到操作数地址大小。 - 如果是写操作,就标记该区域为“已初始化”(尽管不一定真的写了,但没写的一定未初始化,比全都假设初始化好)。 - 还能判断指针出处是否被暴露(如果读操作来自一个指针)。 **局限**:目前只支持x86。其他架构(如ARM)有可伸缩向量指令,大小取决于寄存器值,实现起来太痛苦。“我放弃了。但这个问题会被处理。” ### 从段错误中恢复 有了地址和大小后,需要让程序继续运行。通过ptrace修改寄存器: - 把指令指针设到一个假的extern C函数地址。 - 这个假函数不会解除当前页面的保护,而是返回一个SIGSTOP让父进程捕获。 - “这不是调用约定。我不知道这是什么。这是对计算机的犯罪。” ## 竞态与修复:互斥锁 测试在本地通过,但在CI上总是挂起。原因是Unix信号和IPC通道消息没有同步——竞态的SIGSTOP会在消息到达前被另一个进程收到,导致永远阻塞。 “你们明白为什么我说这像心理剧了吧?我恨它。但我修好了。我通过增加并行度修好了一个竞态条件。” 她使用了多种子Miri(同时运行多个不同RNG种子的进程),然后在本地终于复现了挂起。最终解决方案:在FFI上加一个互斥锁。同一时间只能有一个进程执行FFI。这还顺便修复了Miri一个预先存在的潜在bug:多线程下外部代码同时访问全局变量。 ## 反汇编器:从Capstone到X speaks Capstone是个很好的反汇编器,但Nia需要的不是反汇编——她需要的是一个“能告诉我操作数地址大小的神奇机器”。而且Capstone让check构建变慢。后来通过Oxide的Ixy(X speaks项目)改进了:她骚扰Ixy添加了所需功能,最终X speaks成为更优选择。“据我所知,大部分问题实际上已经被修了。希望我最终能把它放进Miri,这样某天我们就能有更好的跨架构支持。” ## 分配追踪:Shim libC函数 只靠段错误还不足以处理返回指针的场景(例如`malloc`)。Miri需要知道哪些内存是真正有效的分配。她将能访问内存的libffi/libC函数(如`malloc`)都做了shim——预先定义一组extern C的shim函数,在FFI调用时,用ptrace把指令指针跳转到这些假函数里。假函数会调用Miri的分配器,从而让Miri追踪到新分配的内存。 “有人告诉我shim整个libC太过了,完全没必要。好吧,随你们说。” ## 总结:一个由段错误驱动的调试器 “这个‘每秒8000个段错误’的说法真的糟透了。它们迟早会被修好的。” 最终,这套方案不仅让Miri能执行任意FFI,还意外地提供了一个调试器——因为你能精确看到每一次内存访问和崩溃现场。虽然目前只适用于Linux x86,且充满“对计算机的犯罪”,但它确实可行。 **Source:** [FFI in Miri at 8000 segfaults per second - RustWeek](https://youtu.be/9X-ngiKo_Y0)

相似文章

Fil-C: Garbage In, Memory Safety Out

Lobsters Hottest

Fil-C 是一个完全内存安全的 C/C++ 实现,通过 LLVM IR 阶段的不可见能力(invisicaps)将指针值与边界信息捆绑,实现高兼容性,性能损失约 4 倍。

突破防御:利用段错误绕过 Intel CET

Lobsters Hottest

该仓库提供了面向段错误编程(SFOP)的工件,这是一种利用信号处理器绕过 Intel CET 的新型利用技术。它包含针对 Nginx 和 Ladybird 的 PoC 利用和演示。

查询循环:编译器谋杀之谜

Lobsters Hottest

一位 Ferrocene/Rust 编译器工程师详细描述了一场为期一周的调试历程,该崩溃由查询循环引起,最终揭示了三个相互作用的错误,导致了OOM和无限循环。

objdump -g 中的任意代码执行

Lobsters Hottest

objdump -g 中存在一个安全漏洞,由于 FR30 重定位处理程序缺少边界检查,通过精心构造的 FR30 目标文件可实现任意代码执行,单个漏洞利用即可绕过 ASLR 及其他缓解措施。