Rust code auditing
TL;DR: Memory safety covers UAF/double-free/buffer overflow in safe Rust — but logic bugs, integer overflow on release builds, FFI boundaries,
unsafeblocks, serde deserialization with type tags, and supply-chain (build.rs, proc-macros) all remain. Audit those surfaces; the safe code in between is usually fine.
What it is
Rust audits flip the bug ratio: memory-corruption RCE is rare, but unsafe/FFI/Box::from_raw blocks, integer arithmetic without checked_*, and supply-chain build.rs exec are where real bugs hide. Web stacks (axum, actix-web, rocket, warp) add the usual web bug families on top.
Preconditions / where it applies
- Source (
*.rs,Cargo.toml,Cargo.lock) - Edition (2021 / 2024) + MSRV — pattern bindings, async traits differ
- Knowledge of ownership/borrowing, lifetimes, the
unsafecontract
Technique
- Map entry points.
- axum:
Router::new().route("/", get(handler)). - actix-web:
App::new().service(web::resource(...)). - rocket:
#[get("/foo")] async fn handler(...). - warp:
warp::path!("...").and(warp::body::json()). - CLI:
clapargument structs (#[derive(Parser)]).
- axum:
- Audit every
unsafeblock.rg -n 'unsafe\s*\{' src/then read each block’s invariants. The// SAFETY:comment above is the contract — verify it holds across all callers, not just the file-local ones. Common bugs: missing alignment, lifetime extension,transmuteof types with different drop semantics, off-by-one in pointer arithmetic. - Audit FFI. Every
extern "C"and everybindgen-generated wrapper. C lib bugs cross into your Rust UB cleanly.CString::newrejects null bytes — handle the error.from_raw_partsrequires lifetime invariant the borrow checker can’t see. - Integer overflow. Debug builds panic, release builds wrap silently. Audit any arithmetic touching size/offset/index for
checked_add/saturating_*/wrapping_*discipline.a + bwith attacker-controlledaorbin a length field is a classic OOB-write enabler when the result is used as a buffer index. - Serde deserialization.
#[serde(untagged)]enums try variants in order — attacker can pick which Rust type to construct.#[serde(rename_all = "lowercase")]with overlapping field names → confusion.serde_json::Valuethen conversion — type-narrow at the boundary.bincode/ciborium/postcard: binary format integer-overflow on length prefixes.
unwrap/expectaudit. Anywhere in a request path,.unwrap()on attacker input → DoS panic crash. Treat each as a bug; require?orunwrap_or.tokio::spawnpanic doesn’t propagate — silent task death.- Async + locking.
Arc<Mutex<T>>held across.awaitis a deadlock waiting to happen. Usetokio::sync::Mutexfor that case, or scope the lock guard. - Web bug families. axum extractors trust the type system —
Json<Foo>withserde::Deserializewill populate every field; add#[serde(deny_unknown_fields)]to block mass-assignment. SSRF, SQLi (sqlxquery!is safe;querywithformat!is not), template injection (askama/handlebars compile from string), command injection (std::process::Commandwith shell concat) all apply. build.rsand proc-macros. Both run on the dev machine atcargo buildtime. Malicious crate → RCE on your CI / dev box. Audit deps’build.rsand proc-macros for network calls, shell out, env reads, file writes outsideOUT_DIR. Pin transitively inCargo.lock; reviewcargo updatediffs.cargo-auditandcargo-deny. Run in CI.cargo-vet(orcargo-crev) for trust-chain on top crates.- Cryptography.
rand::thread_rng()is CSPRNG;rand::random()is too.RustCryptoaes-gcm/chacha20poly1305for symmetric. Constant-time compare viasubtle::ConstantTimeEq. Avoid hand-rolling block-cipher modes. ring/rustlsaudit. Both are well-reviewed; flag any custom forks.webpkicert chain validation is opt-in for some patterns — check thatverify_is_valid_tls_server_certis called.
Detection and defence
cargo clippy -- -W clippy::pedantic -W clippy::nursery— many security-adjacent lints (integer_arithmetic,panic_in_result_fn).cargo-audit(RustSec advisory DB),cargo-deny(license/source/dup),cargo-vet(trust chain).cargo geiger— countsunsafeuse across the tree; spike = new attack surface.loomfor concurrency model checking on critical paths.- For FFI: pair each
externblock withbindgen --no-derive-debug --no-derive-defaultreview.