Messense Lv

Run cargo test with valgrind

Detect memory errors with Valgrind for Rust programs

As a memory safe language, memory errors are often caught by the Rust compiler in compile time, but when using FFI to interface with C/C++, you often need to use unsafe which can introduce memory unsafety. Valgrind is a valuable tool for working with unsafe Rust, the Rust and Valgrind post by Nicholas Nethercote is a great introduction if you are new to Valgrind.

There are several ways to use Valgrind with Rust, we'll explore them in this post. First let's create an example Rust project with code that leaks memory

$ cargo new example
Created binary (application) `example` package
$ cd example

Replace src/main.rs with the following code

fn leak() {
let data = vec![0; 1024];
std::mem::forget(data);
}
fn main() {
leak();
}
#[cfg(test)]
mod test {
use super::leak;
#[test]
fn test_leak() {
leak();
}
}

Run Valgrind directly

You can just use Valgrind directly on compiled Rust programs, it's quite easy

$ cargo build
Compiling example v0.1.0 (/root/code/example)
Finished dev [unoptimized + debuginfo] target(s) in 1.58s
$ valgrind target/debug/example
==1127561== Memcheck, a memory error detector
==1127561== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==1127561== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==1127561== Command: target/debug/example
==1127561==
==1127561==
==1127561== HEAP SUMMARY:
==1127561== in use at exit: 4,096 bytes in 1 blocks
==1127561== total heap usage: 11 allocs, 10 frees, 6,253 bytes allocated
==1127561==
==1127561== LEAK SUMMARY:
==1127561== definitely lost: 4,096 bytes in 1 blocks
==1127561== indirectly lost: 0 bytes in 0 blocks
==1127561== possibly lost: 0 bytes in 0 blocks
==1127561== still reachable: 0 bytes in 0 blocks
==1127561== suppressed: 0 bytes in 0 blocks
==1127561== Rerun with --leak-check=full to see details of leaked memory
==1127561==
==1127561== For lists of detected and suppressed errors, rerun with: -s
==1127561== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Want to run unit tests with Valgrind? You can do that too

$ cargo test
Compiling example v0.1.0 (/root/code/example)
Finished test [unoptimized + debuginfo] target(s) in 1.37s
Running unittests src/main.rs (target/debug/deps/example-64baa11f6262b62f)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
$ valgrind target/debug/deps/example-64baa11f6262b62f
==1134151== Memcheck, a memory error detector
==1134151== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==1134151== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==1134151== Command: target/debug/deps/example-64baa11f6262b62f
==1134151==
running 1 test
test test::test_leak ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.26s
==1134151==
==1134151== HEAP SUMMARY:
==1134151== in use at exit: 4,096 bytes in 1 blocks
==1134151== total heap usage: 669 allocs, 668 frees, 72,979 bytes allocated
==1134151==
==1134151== LEAK SUMMARY:
==1134151== definitely lost: 4,096 bytes in 1 blocks
==1134151== indirectly lost: 0 bytes in 0 blocks
==1134151== possibly lost: 0 bytes in 0 blocks
==1134151== still reachable: 0 bytes in 0 blocks
==1134151== suppressed: 0 bytes in 0 blocks
==1134151== Rerun with --leak-check=full to see details of leaked memory
==1134151==
==1134151== For lists of detected and suppressed errors, rerun with: -s
==1134151== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Use Valgrind on binary is easy, but for unit tests you need to supply the unit test binary path which can change between builds, and if you have lots of test binaries, it's tedious to find the path and feed it to Valgrind by hand.

Use cargo-valgrind

cargo-valgrind is a cargo subcommand that extends cargo with the capability to directly run Valgrind on any crate executable, it makes running cargo test with Valgrind quite easy.

$ cargo valgrind run
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `/root/.cargo/bin/cargo-valgrind target/debug/example`
Error leaked 4.0 kiB in 1 block
Info at calloc (vg_replace_malloc.c:1328)
at alloc::alloc::alloc_zeroed (alloc.rs:160)
at alloc::alloc::Global::alloc_impl (alloc.rs:171)
at <alloc::alloc::Global as core::alloc::Allocator>::allocate_zeroed (alloc.rs:236)
at alloc::raw_vec::RawVec<T,A>::allocate_in (raw_vec.rs:186)
at alloc::raw_vec::RawVec<T,A>::with_capacity_zeroed_in (raw_vec.rs:139)
at <T as alloc::vec::spec_from_elem::SpecFromElem>::from_elem (spec_from_elem.rs:54)
at alloc::vec::from_elem (mod.rs:2454)
at example::leak (main.rs:2)
at example::main (main.rs:7)
at core::ops::function::FnOnce::call_once (function.rs:248)
at std::sys_common::backtrace::__rust_begin_short_backtrace (backtrace.rs:122)
Summary Leaked 4.0 kiB total
$ cargo valgrind test
Finished test [unoptimized + debuginfo] target(s) in 0.02s
Running unittests src/main.rs (target/debug/deps/example-64baa11f6262b62f)
running 1 test
test test::test_leak ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.26s
Error leaked 4.0 kiB in 1 block
Info at calloc (vg_replace_malloc.c:1328)
at alloc::alloc::alloc_zeroed (alloc.rs:160)
at alloc::alloc::Global::alloc_impl (alloc.rs:171)
at <alloc::alloc::Global as core::alloc::Allocator>::allocate_zeroed (alloc.rs:236)
at alloc::raw_vec::RawVec<T,A>::allocate_in (raw_vec.rs:186)
at alloc::raw_vec::RawVec<T,A>::with_capacity_zeroed_in (raw_vec.rs:139)
at <T as alloc::vec::spec_from_elem::SpecFromElem>::from_elem (spec_from_elem.rs:54)
at alloc::vec::from_elem (mod.rs:2454)
at example::leak (main.rs:2)
at example::test::test_leak (main.rs:16)
at example::test::test_leak::{{closure}} (main.rs:15)
at core::ops::function::FnOnce::call_once (function.rs:248)
Summary Leaked 4.0 kiB total
error: test failed, to rerun pass '--bin example'

cargo-valgrind has a nice colored output and will fail the test if memory errors detected, but as of v2.1.0 it can not output just raw Valgrind output.

Use Cargo runner

Cargo also supports setting a custom target runner which is useful for cross testing, for example, you can cross compile a Rust program from Linux to Windows target and use wine to run unit tests.

We can also leverage this feature to run unit tests under Valgrind:

$ CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER="valgrind --error-exitcode=1" cargo test
Finished test [unoptimized + debuginfo] target(s) in 0.03s
Running unittests src/main.rs (target/debug/deps/example-64baa11f6262b62f)
==1136560== Memcheck, a memory error detector
==1136560== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==1136560== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==1136560== Command: /root/code/example/target/debug/deps/example-64baa11f6262b62f
==1136560==
running 1 test
test test::test_leak ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s
==1136560==
==1136560== HEAP SUMMARY:
==1136560== in use at exit: 4,096 bytes in 1 blocks
==1136560== total heap usage: 669 allocs, 668 frees, 72,998 bytes allocated
==1136560==
==1136560== LEAK SUMMARY:
==1136560== definitely lost: 4,096 bytes in 1 blocks
==1136560== indirectly lost: 0 bytes in 0 blocks
==1136560== possibly lost: 0 bytes in 0 blocks
==1136560== still reachable: 0 bytes in 0 blocks
==1136560== suppressed: 0 bytes in 0 blocks
==1136560== Rerun with --leak-check=full to see details of leaked memory
==1136560==
==1136560== For lists of detected and suppressed errors, rerun with: -s
==1136560== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)