Introduction
Check out our first status update for 2026! đź‘€
BorrowSanitizer is a dynamic analysis tool for detecting Rust-specific aliasing bugs in multi-language applications.
The Rust compiler provides powerful, static safety guarantees by restricting aliasing and mutability. However, developers can bypass these restrictions by using a subset of
unsafe features. These features are necessary for Rust to interoperate with other languages. However, if developers use unsafe code incorrectly, then they can break the rules of Rust’s aliasing model, which the compiler relies on to be able to optimize programs. Incorrect optimizations can introduce security vulnerabilities.
Rust developers can find aliasing bugs using Miri, an interpreter. Miri is the only tool that can find violations of Rust’s latest Tree Borrows aliasing model, but it cannot find these bugs in foreign code. Miri is also significantly slower than native execution, which makes it impractical to use techniques like fuzzing or property-based testing to find these Rust-specific bugs.
BorrowSanitizer is an LLVM sanitizer for finding aliasing violations. Our goal is for it to be fast enough for use with fuzzing tools and to have support for Rust, C, and C++ in interoperation. We intend for it to be a production-ready tool.
đźš§ Our project is still in early stages. BorrowSanitizer is not functional yet. đźš§
Join our Zulip if you are interested in contributing or if you have any additional questions about our project. You can build and test our sanitizer by following the setup instructions in the next section. All of our code is open-source and publicly available on GitHub.
Setup
The easiest way to try BorrowSanitizer is inside a Docker container. Our image supports the following platforms:
| Platform | Target | Description |
|---|---|---|
linux/amd64 | aarch64-apple-darwin | ARM64 macOS (M-series) |
linux/arm64 | x86_64-unknown-linux-gnu | X86 Linux |
First, pull our latest image from GitHub’s container registry.
docker pull ghcr.io/borrowsanitizer/bsan:latest
Then, launch a container and attach a shell.
docker run -it bsan:latest
Once inside the container, you can use our Cargo plugin to build and test crates using BorrowSanitizer.
cargo bsan test
Our plugin supports most of the same subcommands as Miri. When it’s used for the first time, it will perform a one-time setup step of building an instrumented sysroot. You can trigger this step manually using the setup subcommand.
Building from Source
Every single command needed to build, test, and install BorrrowSanitizer can be accessed through xb, our build script. For first-time setup, run:
xb setup
If you only want to install BorrowSanitizer, then run:
xb install
This will install a custom Rust toolchain under the name bsan. You can speed this up by building our dev container, which already has the bsan toolchain installed. We recommend using the container to avoid any environment-specific issues.
You can build and test components of the project using the build and test subcommands. For example, running xb build will build everything, but you can also pass the name of a subdirectory to build just that component, like so:
xb build bsan-rt
Nearly every subcommand can be used this way.
After making a change, you should run all of our CI steps locally using:
xb ui
This will place our binaries into Cargo’s home directory ($HOME/.cargo). You will need to have bsan set as the active toolchain (e.g. rustup default bsan) for our tool to work.
Status Update - January 2026
Happy New Year! Moving forward, we will be posting monthly status updates—starting with this one. Here’s a brief overview:
✨What’s new?
We have posted an MCP for our changes to the compiler, which are mostly stable. The core design principles of our runtime and instrumentation pass are either fully implemented or well thought-out. We have been testing BorrowSanitizer using the single-threaded programs from a subset of Miri’s test suite that deals with aliasing violations, and we have similar outcomes; tests either pass or report an error in both Miri and BorrowSanitizer.
âŹď¸Ź What’s next?
We are currently implementing garbage collection, support for atomics, and error reporting. Once these features are merged, we will be able to expand our support to the rest of Miri’s test suite, and we will begin auditing failing tests to make sure that we are finding the same errors as Miri. We will begin automated testing and benchmarking on real-world libraries. Expect to see benchmarking results in our February update.
We discuss these points in more detail below. We will continue to post monthly updates here and in our project goal.
Compiler Changes
Retags are the central mechanism of Stacked and Tree Borrows. Broadly, when a reference is created, assigned, or passed into a function, its underlying pointer is “retagged”, creating a new permission in the stack or tree. Miri uses a combination of MIR instructions and type information to determine when a retag needs to occur, but this information is lost at the LLVM level. This would ordinarily prevent us from distinguishing between raw pointers and references, and we need to do this to be able to find aliasing violations.
We modified the Rust compiler to emit retags as function calls. If we have the following MIR:
x = &mut y;
We emit the following function call:
%2 = @__retag(ptr %1, ...)
We use a fork of the compiler with a prototype of these changes in our current implementation of BorrowSanitizer.
In December, we posted a pre-RFC describing these changes, which was adapted into the MCP that we posted earlier this week. Our current prototype depends on MIR
Retag instructions to determine where to emit retags during codegen, but these instructions are likely going away, so the final version of our changes will determine where to insert these instructions during codegen. We expect these changes to be relatively straightforward, so barring any unexpected concerns, we should be able to merge our changes before the start of the second project goal period in April.
Once these changes are merged, the rest of development can continue out-of-tree. However, there’s another relevant proposal that would be helpful to have implemented to support BorrowSanitizer. Last September, an MCP was been accepted for including clang as a Rust component. Our instrumentation pass uses the distribution of LLVM from nightly, so we need clang to have a matching version to be able to instrument C and C++ programs. Having clang as part of the user’s sysroot would make this far easier.
Remaining features
Three critical features have yet to be finished before we can expand our testing to the rest of Miri’s test suite and other crates. The following list is not comprehensive—there are other features that we have yet to support, such as thread-local storage, global variables, and constant allocations. However, we expect that these will be straightforward extensions of existing functionality, and not core features like the following:
Error Reporting
When Miri finds an aliasing violation, it reports both the primary location where the error occurred and the secondary locations of memory accesses that indirectly caused the error. Providing a backtrace for the primary location is straightforward, and we can record the instruction pointers for the secondary locations within the nodes of the tree. However, we have not implemented a method for translating these instruction pointers into source locations. We are currently adding support for using llvm-symbolizer to handle this, and once it is finished, we will add back nicer error messages so that we can start auditing failing test cases.
Garbage Collection
Periodically, Miri “stops the world” and collects all of the permissions associated with pointers stored in memory. Then, it visits the stack or tree for each allocation and removes permissions that are no longer reachable in memory. This can dramatically improve performance.
Like Miri, we can scan our shadow memory spaces for provenance values. However, instead of halting execution, BorrowSanitizer’s approach will continue to allow threads to execute. A thread will only be blocked when it attempts to update shadow memory. All other operations, including certain run-time checks, will still be able to continue concurrent to garbage collection.
We can already scan through our “shadow heap”, which contains provenance values that are stored to memory. However, we still need a way to track which provenance values are accessible on the stack. At the moment, we load provenance into virtual registers, just like any other LLVM value. This is convenient to implement, but it prevents us from knowing where provenance is stored at run-time. At the moment, we are switching to using a “shadow stack” to store provenance values. Garbage collection will block any operations that updates the shadow stack pointer.
Concurrency
Each provenance value is three words: an allocation ID, a borrow tag, and a pointer to an allocation metadata object. Every regular load or store of a pointer requires three loads or stores for each component of its provenance. This is potentially subject to data races, but that’s actually fine in this case. Any race on a provenance value would also be a race on the pointer’s value, and that would be a problem for ThreadSanitizer to detect.
However, we cannot use the same approach for atomic pointers. If we atomically stored each component of a provenance value in succession, then this would not guarantee that all three values would be read consistently. We would be introducing a data race in our runtime that does not exist in the original program. When provenance values are read in an inconsistent state, this all but guarantees a false positive error.
We have two options to address this. The easiest approach would be to block shadow memory on atomic accesses. This is expensive, but we can mitigate it somewhat by only blocking the individual page of memory in our shadow page table. Another option would be to remove allocation IDs from our provenance values altogether, and use the allocation info pointer as an ID instead. In that case, provenance values would be 128 bits, which is small enough for atomic loads and stores. Support for atomic operations on 128-bit integers is inconsistent but improving, and it could be worth the performance increase.
Evaluation
We do not have a clear idea of what performance looks like yet—we’ve been focused on feature implementation over benchmarking. At the moment, we are slower than Miri for certain programs. We can attribute this to our lack of garbage collection and our eagerness in locking global state to validate accesses. Expect to see initial benchmark results in our February update. Additionally, one of our team members—@Gitter499—has been working on a dashboard to display performance results that we can integrate into CI. We also plan on running an instance of crater once we can figure out a configuration that works with our driver.
Conclusion
Let us know if you have any questions! You can reach us on Zulip. Otherwise, you’ll hear from us again in February for our next update.
We thank Google and the Rust Foundation for funding this project, as well as @RalfJung and Tyler Mandry for their time and effort in providing feedback.
About
If you have questions or are interested in contributing, the best way to reach us is on Zulip. You can also send us an email and one of our team members will follow up with you. BorrowSanitizer is open source and publicly available on GitHub.
Talks & Publications
We presented our work at the following venues:
Team
Please reach out to us if you are interested in contributing to this project. Current and past contributors include (in no particular order):
- Ian McCormack
- Oliver Braunsdorf
- Aaron Tan
- Johannes Kinder
- Jonathan Aldrich
- Joshua Sunshine
- Rafayel Amirkhanyan
- Molly MacLaren
License & Credits
BorrowSanitizer is dual-licensed under Apache and MIT.
Zulip sponsors free hosting for BorrowSanitizer. Zulip is an organized team chat app designed for efficient communication.