Development Guide
BorrowSanitizer uses several extensions to the Rust and LLVM toolchains.
Toolchain | Component | ./x.py build | Link |
---|---|---|---|
Rust | Cargo Plugin | cargo-bsan | |
Rust | Rustc Plugin | bsan-driver | |
LLVM | Instrumentation Pass | - | |
LLVM | Sanitizer Interface | bsan-rt | |
Rust | Sanitizer Runtime Library | bsan-rt |
System Overview
Our Cargo plugin builds an instrumented sysroot by redirecting compiler invocations through our Rustc plugin, which adds the flags for enabling BorrowSanitizer. This saves time when switching between projects. Otherwise, users would need to rebuild Rust’s standard library by passing -Zbuild-std
for every project that they want to use with BorrowSanitizer. Any static optimizations that require Rust-specific type information will be implemented within the Rustc plugin.
We modified the Rust compiler to accept borrow
as one of the options for -Z sanitizer=...
. If this option is enabled, then the compiler will emit “retag” instructions, which create new permissions for references. This has a effect similar to the flag -Zmir-emit-retag
, which Miri uses to emit retags. However, Miri only emits retags for a subset of operations, the rest are executed implicitly as side-effects of the interpreter. We emit retags everywhere that they are necessary for Tree Borrows. Our retags are lowered to LLVM as calls to a new @llvm.retag
intrinsic function. Our LLVM insturmentation pass replaces this intrinsic with a call to our runtime library. Its arguments will capture all of the Rust-specific type information that we need to instrument LLVM IR for detecting aliasing violations. All other run-time checks are inserted in the backend by the LLVM pass.
Our runtime library is implemented as two components. The first component is implemented in C++ using the LLVM Sanitizer API. This component redirects our runtime checks to a Rust runtime library, where we implement the semantics of Rust’s aliasing model and all necessary operations for initializing and accessing values in shadow memory. This design allows us to reuse Miri’s implementation of Tree Borrows. It will also make it easier to port our approach to other Rust backends, since developers will only need to reimplement our instrumentation pass. Eventually, we will distribute these libraries as LLVM IR, which our Cargo plugin will statically link against the source program using cross-language LTO. This will allows us to gain performance through inlining. They are dynamically linked for now, though.
Configuration
Builds of the Rust compiler are configured through a
config.toml
file in the root directory of the repository.
This file is auto-generated by running ./x.py setup
. BorrowSanitizer, you need to set the following configuration options:
[build]
extended = true
tools = ["bsan-rt", "cargo-bsan"]
[llvm]
download-ci-llvm = false
[rust]
llvm-tools = true
This ensures that LLVM is built from source so that you have access to our instrumentation pass. We also recommend setting clang=true
under this section. This builds our modified version of Clang, which can be configured to enable BorrowSanitizer for C/C++ programs.
We provide a sample configuration file for you to modify. If you are creating a distribution build, then you should reuse Rust’s default configuration.
Faster LLVM Builds
Changes to LLVM require rebuilding parts of the frontend. This can make it difficult to rapidly test changes to the backend instrumentations pass. For faster rebuilds, we recommend following these steps:
First, remove the build directory if it exists.
rm -rf ./build
Then, add the following setting to your configuration file:
[llvm]
link-shared=true
This ensures that LLVM will be built as a dynamically linked library. Now, build LLVM:
./x.py build llvm
Afterward, add the absolute path to ./build/host/llvm/lib
so that LLVM is visible to the dynamic linker.
export LD_LIBRARY_PATH="$(cd build/host/llvm/lib && pwd):$LD_LIBRARY_PATH"
If you are compiling on macOS, then you will also need to add this path to the variable “LD_RUNPATH_SEARCH_PATH”.
Testing & CI
We have unit tests for our runtime library and UI tests for the entire toolchain. Our unit tests are within bsan-rt
, and our UI tests are within bsan-driver
. You can execute each test suite by passing the relative path to these components as an argument to ./x.py test
, like so:
./x.py test src/tools/bsan/bsan-rt
./x.py test src/tools/bsan/bsan-driver --stage 2
Our CI pipeline checks for formatting and executes our tests on each of our supported architectures. Run ./x.py fmt
before opening a pull request. All pull requests should be made using the branch
BorrowSanitizer/bsan
as the base for comparison. Make sure to select the correct branch when using GitHub, since the interface defaults to submitting a pull request against the main branch of the Rust toolchain! All pull requests must pass our CI pipline. External pull requests must be reviewed by a member of the team.