From 857d31ab332c156d907c76fee68f1b8f8571b79f Mon Sep 17 00:00:00 2001 From: Dana Robinson Date: Sat, 17 Oct 2026 20:35:38 -0700 Subject: [PATCH] Add a valgrind suppression file for the tests test/valgrind.supp suppresses the "still reachable" blocks that valgrind reports in the unit tests and examples when it's run with --show-leak-kinds=reachable or all. None of them come from a bug in hipObject; each is memory that a library allocates once and keeps until the process exits: - The static constructors in ROCm's libLLVM and libclang-cpp, which the HIP runtime loads - The HIP runtime, while it's being loaded and during its one-time setup on the first HIP call (including hsa_init()) - systemPlatform(), which never destroys its platform, and the dynamic loader's records of libibverbs, which the platform keeps loaded - OpenSSL's global state, which it keeps after curl_global_cleanup() Every entry matches only reachable blocks, so definite, indirect, and possible leaks are always reported. The HIP and systemPlatform() entries match only allocations made under the function that does the one-time setup, so the same allocations made anywhere else are still reported. Some of those stacks are about 20 frames deep, so the file says to run valgrind with --num-callers=40 or more. The file also says that valgrind can't run optimized Clang builds of hipObject correctly. With -ftrivial-auto-var-init=pattern, which hipObject's Clang options always add, Clang at -O1 and above copies std::format()'s arguments with the x87 instructions fldt and fstpt. Valgrind emulates the x87 registers with 64 bits instead of 80, so the copy loses bits: std::format("{}", 42) returns "0", and formatting a string can crash. A five-line std::format() program reproduces it, even with --tool=none, and runs correctly natively and with GCC. GCC builds and Clang Debug builds work. Testing: ran ctest -T memcheck on the 26 unit tests and the get-version example with --leak-check=full --show-leak-kinds=all --errors-for-leak-kinds=all --num-callers=40, with ROCm 10.1 and valgrind 3.26. GCC Debug and Clang Debug builds passed 27/27, with no errors and no blocks still reachable. Without the file, all 27 fail. codespell is clean. Co-Authored-By: Claude --- test/valgrind.supp | 96 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 test/valgrind.supp diff --git a/test/valgrind.supp b/test/valgrind.supp new file mode 100644 index 00000000..e1263f2e --- /dev/null +++ b/test/valgrind.supp @@ -0,0 +1,96 @@ +# Valgrind suppressions for hipObject's tests. +# +# Use this file with valgrind's --suppressions option. With CTest, configure +# with -DMEMORYCHECK_SUPPRESSIONS_FILE= and then run +# "ctest -T memcheck". +# +# Every entry here suppresses only "still reachable" blocks. Valgrind doesn't +# report those unless it's run with --show-leak-kinds=reachable or +# --show-leak-kinds=all. Leaks that are definitely, indirectly, or possibly +# lost are never suppressed. +# +# Each entry covers memory that a library allocates once and keeps until the +# process exits. Most entries match only allocations made under the function +# that does that one-time setup, so the same allocations made anywhere else +# are still reported. Those stacks can be deeper than valgrind's default of 12 +# frames, and a stack that's cut off before the function doesn't match, so +# run valgrind with --num-callers=40 or more. +# +# Valgrind can't run optimized Clang builds of hipObject correctly, so use a +# GCC build or a Clang Debug build. hipObject builds with +# -ftrivial-auto-var-init=pattern when it's compiled with Clang. With that +# flag, Clang at -O1 and above copies std::format()'s arguments with the x87 +# instructions fldt and fstpt. Valgrind emulates the x87 registers with 64 +# bits instead of 80, so the copy loses bits: formatted numbers come out +# wrong, and formatting a string can crash. No suppression can fix that. + +# The HIP runtime loads ROCm's LLVM and clang libraries. When those libraries +# load, their static constructors allocate memory that they never free. +# hipObject never calls LLVM directly; LLVM is loaded only as part of the HIP +# runtime. So any reachable block that LLVM allocated belongs to ROCm. +{ + rocm-libLLVM-reachable + Memcheck:Leak + match-leak-kinds: reachable + ... + obj:*/libLLVM.so* +} + +{ + rocm-libclang-cpp-reachable + Memcheck:Leak + match-leak-kinds: reachable + ... + obj:*/libclang-cpp.so* +} + +# hipObject does call the HIP runtime, so the next two entries only cover +# the runtime's one-time setup. A reachable block that HIP allocates at any +# other time, during a call from hipObject, could mean that hipObject didn't +# release something, so it is still reported. + +# Memory the HIP runtime allocates while the dynamic loader (ld.so) is +# loading it +{ + rocm-libamdhip64-load-time-reachable + Memcheck:Leak + match-leak-kinds: reachable + ... + obj:*/libamdhip64.so* + ... + obj:*/ld-linux*.so* +} + +# The first HIP call initializes the runtime once, with pthread_once(). This +# includes the HSA runtime's setup, in hsa_init(). +{ + rocm-libamdhip64-first-call-reachable + Memcheck:Leak + match-leak-kinds: reachable + ... + obj:*/libamdhip64.so* + fun:*pthread_once* +} + +# systemPlatform() builds the platform that hipObjInit() uses the first time +# it's called, and deliberately never destroys it (see src/state.cpp). The +# platform keeps libibverbs loaded with dlopen(), so the platform's objects +# and the dynamic loader's records of libibverbs and its dependencies stay +# allocated until the process exits. +{ + hipobj-system-platform-reachable + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:*systemPlatform* +} + +# OpenSSL, which libcurl uses, keeps some of its global state after +# curl_global_cleanup(), until the process exits. +{ + openssl-global-init-reachable + Memcheck:Leak + match-leak-kinds: reachable + ... + fun:OPENSSL_init_crypto +}