Memory safety bugs in cryptographic software
Heartbleed in OpenSSL, goto fail in GnuTLS, side-channel leaks in libgcrypt: cryptographic software has a long history of vulnerabilities caused by memory safety issues in C and C++.
The algorithms were sound. The implementations failed, because the languages make it too easy to write dangerous code.
How common these bugs are
Microsoft's security team analyzed decades of CVEs and found that 70% of security vulnerabilities are memory safety issues. Google's Chrome team reports similar numbers. For cryptographic libraries the percentage is even higher, because:
- Crypto code handles secrets: a buffer overflow leaks keys rather than only crashing.
- Performance pressure: optimizations often involve unsafe memory operations.
- Complex algorithms: more code means more opportunity for mistakes.
Memory safety CVEs in cryptographic libraries
| Library | CVE | Type | Impact |
|---|---|---|---|
| OpenSSL | CVE-2014-0160 | Buffer over-read | Private key disclosure |
| GnuTLS | CVE-2014-3466 | Buffer overflow | Remote code execution |
| libgcrypt | CVE-2017-7526 | Side-channel | RSA key recovery |
| wolfSSL | CVE-2020-12457 | Buffer overflow | Denial of service |
| mbedTLS | CVE-2021-44732 | Double free | Remote code execution |
How Rust prevents these bugs
Rust's ownership system prevents memory safety bugs at compile time.
Buffer overflows
In C, nothing stops you from reading past array bounds:
// C: Compiles fine, causes Heartbleed-style vulnerability
void process_message(uint8_t *buf, size_t claimed_len) {
uint8_t response[MAX_SIZE];
// If claimed_len > actual length, we read arbitrary memory
memcpy(response, buf, claimed_len);
send_response(response, claimed_len);
}
In Rust, the compiler enforces bounds checking, so buffer overflows are impossible:
// Rust: Won't compile if bounds aren't checked
fn process_message(buf: &[u8]) -> Vec<u8> {
// buf.len() is always accurate - can't lie about length
let mut response = Vec::with_capacity(buf.len());
response.extend_from_slice(buf);
response
}
Use-after-free
C allows dangling pointers, and a single use-after-free can enable remote code execution:
// C: Use-after-free vulnerability
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
// ... use ctx ...
EVP_CIPHER_CTX_free(ctx);
// ... later, accidentally ...
EVP_EncryptUpdate(ctx, out, &outlen, in, inlen); // UAF!
Rust's ownership system prevents this entirely:
// Rust: Won't compile - ctx is moved/dropped
let ctx = CipherCtx::new();
// ... use ctx ...
drop(ctx);
// ctx.encrypt(&input)?; // Compile error: use of moved value
Data races
Concurrent access to shared state causes subtle bugs:
// C: Data race on shared key state
void encrypt_parallel(key_state *state, uint8_t *data[], size_t count) {
#pragma omp parallel for
for (size_t i = 0; i < count; i++) {
// Multiple threads modify state simultaneously - UB
aes_encrypt(state, data[i]);
}
}
Rust's type system enforces thread safety, so data races are impossible:
// Rust: Either shared (&T) or mutable (&mut T), never both
fn encrypt_parallel(key: &AesKey, data: &mut [&mut [u8]]) {
data.par_iter_mut().for_each(|block| {
// key is shared (immutable), blocks are exclusively accessed
key.encrypt_block(block);
});
}
Performance compared with C
A common misconception is that safety costs performance. Rust proves otherwise.
Zero-cost abstractions
Rust's abstractions compile away to the same machine code as hand-written C:
// High-level Rust
pub fn xor_blocks(a: &mut [u8; 16], b: &[u8; 16]) {
for i in 0..16 {
a[i] ^= b[i];
}
}
// Compiles to the same assembly as:
// void xor_blocks(uint8_t *a, const uint8_t *b) {
// for (int i = 0; i < 16; i++) a[i] ^= b[i];
// }
Benchmark: OpenCrypt vs C libraries
| Operation | OpenCrypt (Rust) | OpenSSL (C) | libsodium (C) |
|---|---|---|---|
| AES-256-GCM 1KB | 0.82 μs | 0.78 μs | 0.85 μs |
| SHA-256 1KB | 1.21 μs | 1.18 μs | 1.25 μs |
| X25519 | 48.3 μs | 45.9 μs | 47.1 μs |
| Ed25519 Sign | 52.7 μs | 50.2 μs | 51.8 μs |
Rust matches or exceeds C performance while eliminating memory safety bugs.
Rust cryptography libraries
The Rust community has built a solid cryptography ecosystem.
RustCrypto
A collection of pure-Rust cryptographic primitives:
- AES, ChaCha20, Salsa20 ciphers
- SHA-2, SHA-3, BLAKE3 hashes
- RSA, ECDSA, Ed25519 signatures
- Well-audited, constant-time implementations
ring
A focused library that uses BoringSSL's verified assembly:
- Production-grade TLS primitives
- Used by major projects like Cloudflare
- Hybrid Rust/assembly approach
OpenCrypt
Our high-performance library, with post-quantum support:
- Pure Rust, zero unsafe code
- ML-DSA, ML-KEM implementations
- Formally verified core operations
Writing secure crypto code in Rust
Even in Rust, cryptographic code needs care.
Constant-time comparison
Use the subtle crate for timing-safe comparisons:
use subtle::{ConstantTimeEq, Choice};
fn verify_mac(computed: &[u8; 32], received: &[u8; 32]) -> bool {
// Constant-time comparison - no early exit
computed.ct_eq(received).into()
}
Zeroizing secrets
Zeroize secrets when done:
use zeroize::Zeroize;
struct SecretKey {
bytes: [u8; 32],
}
impl Drop for SecretKey {
fn drop(&mut self) {
self.bytes.zeroize(); // Overwrites memory on drop
}
}
Branching on secrets
Be careful with conditional logic:
// BAD: Timing leak through branching
fn select_bad(condition: bool, a: u64, b: u64) -> u64 {
if condition { a } else { b } // Branch predictor leaks condition
}
// GOOD: Constant-time selection
fn select_good(condition: bool, a: u64, b: u64) -> u64 {
let mask = -(condition as i64) as u64;
(a & mask) | (b & !mask)
}
Migrating from C cryptographic libraries
Phase 1: new code in Rust
Write new cryptographic code in Rust and expose C-compatible interfaces with cbindgen:
#[no_mangle]
pub extern "C" fn opencrypt_aes_gcm_encrypt(
key: *const u8,
nonce: *const u8,
plaintext: *const u8,
plaintext_len: usize,
ciphertext: *mut u8,
) -> i32 {
// Safe Rust implementation called from C
}
Phase 2: gradual replacement
Replace C components one at a time, validating behavior with extensive testing.
Phase 3: pure Rust
Eventually remove C dependencies entirely, for full memory safety guarantees.
We think Rust should be the default for new cryptographic projects, and that migrating existing ones is worth the investment. Every line of OpenCrypt is written in safe Rust.