Memory safety vulnerabilities are one of the biggest challenges we face as developers. For years, we’ve relied on C++ as a trusted workhorse for building complex systems. But managing memory manually in C++ brings constant risk of bugs that jeopardize security and stability. As reported by Microsoft, Google, and others, memory safety vulnerabilities account for 70% or more of vulnerabilities in our codebases.
At RunSafe Security, I had the opportunity to lead the transition of our 30k lines of C++ codebase to Rust. Two things influenced our decision:
- RunSafe wanted to address C++ memory safety concerns, and signed CISA’s Secure by Design Pledge, which includes transitioning to memory safe languages.
- We believe that security software, which RunSafe provides, should be held to a higher level of scrutiny and correctness guarantees. For us, that meant choosing the right programming language was as important as the code itself.
The transition wasn’t without its challenges. Converting a large, established C++ codebase to Rust required careful planning, creative problem-solving, and plenty of patience. In this blog, I’ll walk you through why we chose to make the switch, the obstacles we encountered along the way, and the results we achieved. I hope these insights provide value to anyone considering a similar journey.
What Motivated the Conversion of C++ to Rust?
RunSafe chose the Rust programming language because of several advantages it offers.
- No undefined behavior
- Stronger type system
- Memory safety guarantees
- No data races
The most important advantages from our perspective were the combination of memory safety and lack of garbage collection.
Rust’s advantages extended beyond security. We also saw opportunities for:
- Cleaner, more reliable code
- Stronger compile-time guarantees
- Better performance optimization without the overhead of garbage collection
Challenges in Converting to Rust
Migrating a C++ codebase to Rust is not a decision without obstacles. For RunSafe, challenges stemmed from both technical limitations and philosophical differences in how C++ and Rust approach certain concepts.
Code and Library Compatibility Issues
- Templating vs. Generics: While C++ templates offer flexibility, they don’t cleanly map to Rust’s generic programming model, requiring manual adaptation.
- Standard Library Dependencies: Given RunSafe’s need to avoid libc dependencies for enhanced security, Rust’s #[no_std] environment became necessary. This stripped-down configuration added constraints, particularly for syscalls and memory allocations.
Dealing with Mutable State
C++ often permits unrestricted mutability, allowing developers to directly manipulate global state. Rust’s borrow checker, which enforces ownership rules, fundamentally rejects this assumption. Overcoming this required rewriting significant portions of the codebase to adhere to Rust’s stricter ownership principles.
Platform Support
C++’s compatibility with a wide range of platforms, including esoteric ones, is unmatched. Rust’s smaller ecosystem and more limited targets presented a hurdle when considering some low-level platforms that RunSafe’s software relied on.
Steps to Transition Your Code from C++ to Rust
If you’re considering converting a C++ codebase to Rust, I recommend taking a structured approach to cover all your bases. Here’s how RunSafe managed the transition:
Step 1. Evaluate and Prepare
- Review and improve test coverage to ensure consistent behavior after migration.
- Document core functionalities and interfaces clearly to plan the transition effectively.
Step 2. Plan the Conversion Approach
- Choose between manual or automated conversion. Tools like c2rust offer automated solutions, though the output may require significant refinement to fit idiomatic Rust.
- Implement an incremental conversion strategy where sections are rewritten piecemeal, avoiding wholesale changes that could introduce larger risks.
Step 3. Build a Skeleton in Rust
- Start by creating structural outlines (e.g., functions, modules) in Rust, leaving placeholders like unimplemented!() where details are pending.
- This early step helps identify ownership or mutability conflicts that need resolution.
Step 4. Refactor and Optimize
- Use Rust-specific tools like clippy to make your code cleaner and more idiomatic.
- Replace sentinel values with Rust constructs like Option or Result for better error handling.
Outcomes: What RunSafe Found
Overall, we found the transition to Rust to be successful. Key outcomes included:
- Timing: Took one engineer about 3 months of conversion time for 30k lines of code and an additional 3 months of ironing out integration bugs on esoteric platforms.
- Bugs: We found many bugs, including an incorrect argument type ported from a C++ syscall that caused visible stack corruption failures in Rust when optimized and C++ Case statements that “fell through” when they should have produced a value like Rust’s match statement.
- Size: We saw a 35% reduction in file sizes overall.
- Performance: Initially, we saw 2x slower than C++ implementation, but used profiling to lead us to targets that we would have otherwise not seen. After addressing these issues, Rust speed was roughly on par with C++.
- Safety: Required some unsafe code, which is now all behind safe abstractions.
- Good Surprises: We were confident in refactoring during this process. Moving, editing, and restructuring is substantially easier to verify when everything is more strictly checked, though it may require more effort upfront.
Should You Rewrite Your Code in Rust?
The answer depends on how critical memory safety is to your project and how often you find yourself chasing bugs caused by undefined or unreliable behavior in C++. The key question is: how much of your code actually needs rewriting?
Here are some critical factors to consider:
- Project Scope: How much of your codebase is critical enough to warrant the transition? Small, security-critical sections may provide the greatest ROI.
- Development Resources: Do your developers have experience with Rust, or will training be needed?
- Compatibility Needs: If your software targets esoteric platforms supported by C++, this may limit Rust’s viability.
- Cost and Timeline: Migration requires upfront investment, but the long-term benefits of reduced debugging and increased security often justify the effort.
Rewriting an entire million-line codebase isn’t realistic—it’s neither cost-effective nor time-efficient. However, you might identify smaller, high-risk sections of your codebase that are prone to memory safety issues. Even rewriting small portions of critical code can be enough to reduce your bug surface area.
Learn more about memory safe coding practices and solutions to defend your codebase in this white paper: “A Comprehensive Guide to Addressing the Memory Safety Crisis.”