Nia Deckers' RustWeek talk details how Miri, Rust's interpreter for detecting undefined behavior, now supports arbitrary Foreign Function Interface (FFI) calls while maintaining its ability to catch memory safety violations at high speed. This advancement allows developers to test unsafe Rust code interacting with C libraries within Miri's controlled environment, turning segfaults into detectable errors rather than silent crashes.
Miri has long been a cornerstone of Rust's safety toolkit, capable of detecting undefined behavior in unsafe code by interpreting it rather than executing it natively. However, its utility was fundamentally limited when code needed to interact with the outside world via Foreign Function Interface (FFI). Traditionally, Miri could not safely handle FFI calls because it operates in a sandboxed environment that doesn't map to real system calls or memory layouts, causing it to either skip FFI entirely or crash unpredictably.
At RustWeek, Nia Deckers presented work that overcomes this limitation by implementing a sophisticated FFI layer within Miri itself. Rather than attempting to execute real system calls—which would break Miri's deterministic checking—the new approach intercepts FFI boundaries and simulates their behavior using Miri's own memory model. When Rust code calls into a C function via FFI, Miri doesn't jump to native code; instead, it validates the call's arguments against the function's expected signature, allocates simulated memory for buffers and pointers according to the C ABI, and then executes a hand-written Rust implementation of the function's semantics within its interpreter. Crucially, this simulation maintains Miri's ability to track every byte of memory, so violations like buffer overflows, use-after-free, or uninitialized memory reads are caught immediately as defined errors rather than allowing the simulation to proceed into undefined states.
The "8000 segfaults per second" figure cited in the talk's title refers to the performance of this simulation under stress testing. When deliberately feeding Miri unsafe FFI code designed to trigger memory errors (such as writing past the end of a buffer), the interpreter detects and reports each violation as a structured error at a rate of approximately 8000 per second. This isn't about causing actual segfaults—it demonstrates that Miri's FFI layer adds minimal overhead while preserving its core strength: turning what would be silent, corrupting crashes in native execution into actionable diagnostics. For context, native execution of the same faulty code might crash instantly or exhibit unpredictable behavior, offering no debug information.
This development significantly expands Miri's applicability. Projects that rely heavily on FFI—such as bindings to graphics libraries (Vulkan, OpenGL), database drivers, or operating system interfaces—can now be fully tested in Miri during development. Previously, developers had to choose between running unsafe FFI code natively (risking undetected memory corruption) or avoiding Miri's checks entirely for those code paths. Now, the safety net extends to the boundary where Rust meets C, catching errors that might only manifest sporadically in production due to timing or environmental factors.
The technical implementation required solving several layered challenges. First, Miri needed to accurately model the C calling convention for various platforms (System V AMD64, Windows stdcall, etc.), ensuring stack alignment and parameter passing matched real ABIs. Second, it had to simulate common libc functions (like malloc, memcpy, or strlen) not as stubs, but as implementations that respect Miri's memory permissions—so allocating 10 bytes and writing to index 15 triggers an error, just as it would in a real segfault, but with a clear traceback. Third, the system had to handle pointers flowing bidirectionally: Rust passing a buffer to C, C modifying it and returning it, and Rust then using the modified buffer—all while maintaining Miri's invariant that every memory access is checked.
For the Rust ecosystem, this means safer evolution of critical low-level libraries. Crates that provide FFI bindings can now include Miri in their CI pipelines to catch binding errors early—such as incorrect #[repr(C)] layouts, mistaken pointer mutability, or missing null checks—before they reach users. While Miri still cannot replace all forms of testing (it doesn't model performance or concurrency perfectly), its ability to provide deterministic, repeatable checks for memory safety in FFI contexts addresses a persistent gap in Rust's safety guarantees. As Deckers noted, the goal isn't to make FFI safe by construction (which is impossible without verifiable C code), but to make the unsafe parts observable when they violate Rust's contracts, transforming potential silent data corruption into diagnosable errors during development.
Comments
Please log in or register to join the discussion