Run cargo test with valgrind

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)

爱发电上赞助