Julian Seward first released Valgrind publicly in 2002, and more than two decades later it remains one of the few tools that can find a use-after-free bug in a C binary without recompiling a single line of source. Valgrind works by running your program on a synthetic CPU through dynamic binary instrumentation, rewriting machine code on the fly so every memory access and every value computed can be checked. Memcheck, its default and most widely used tool, is the part that actually hunts memory bugs — invalid heap and stack accesses, reads of uninitialized values, mismatched allocator calls, and leaks the process never freed. The tradeoff is well documented in Valgrind's own manual: instrumented programs run 10-50x slower than native execution, which is why nobody ships Valgrind to production and everybody who works in C or C++ eventually reaches for it in CI or before a release. The instrumentation core also powers Cachegrind, Callgrind, Helgrind, DRD, and Massif — profilers and race detectors built on the same substrate. This post walks through what Memcheck actually catches, how to run it, and when AddressSanitizer is the better tool for the job.
What does Memcheck actually detect?
Memcheck detects the memory bug classes that crash C programs unpredictably or leak them into swap over time: use-after-free (reading or writing heap memory after it's been freed), invalid reads and writes past the bounds of a heap block, stack, or global array, use of uninitialized values in conditionals or as pointers, mismatched allocation and deallocation (calling free() on memory obtained with new, or double-freeing the same pointer), and memory leaks where allocated blocks become unreachable before the program exits. It does this by shadowing every byte of memory with metadata tracking whether it's addressable and defined, then checking that metadata on every load, store, and system call. This is why Memcheck can catch a use-after-free that only corrupts data three function calls later, long after a simple crash-on-write bug would have gone silent — the kind of bug that a compiler warning or a code reviewer has no realistic way to spot by inspection.
How do you actually run it against a real program?
You run Memcheck against an already-compiled binary — no special build flags, no recompilation, no relinking — with a command like valgrind --leak-check=full --show-leak-kinds=all ./myprogram . The --leak-check=full flag tells Memcheck to report the exact location of every leaked block rather than a summary count, and --show-leak-kinds=all includes "still reachable" blocks alongside definite and indirect leaks. For CI pipelines, --error-exitcode=1 makes Valgrind return a nonzero exit code whenever it finds an error, so a build step can fail the pipeline automatically instead of relying on someone reading scrollback. Compiling with -g first is strongly recommended, since debug symbols let Memcheck print the exact source file and line number for each invalid access rather than a bare instruction address, which turns a report from "somewhere in libc" into "line 214 of parser.c."
Why is a memcheck run so much slower than the program itself?
A Memcheck run is 10-50x slower than native execution, according to Valgrind's own documentation, because it isn't sampling or spot-checking — it's re-emitting every single instruction through its instrumentation layer and updating shadow memory on every access. Where a profiler might sample the call stack a few hundred times a second, Memcheck inserts validity checks around every load and store in the program, plus every function call into the allocator. That overhead is the reason Valgrind runs are typically confined to test suites, fuzzing corpora replay, or dedicated CI jobs rather than a developer's everyday build-and-run loop, and why a large integration test suite that takes two minutes natively might take an hour and a half under Memcheck. Teams that adopt it for real workloads generally run it on a subset of tests or on a nightly job rather than on every commit, treating it as a periodic deep check rather than a fast feedback loop.
How does Valgrind compare to AddressSanitizer?
Valgrind and AddressSanitizer (ASan) both catch overlapping bug classes but take opposite architectural approaches, which is the real reason teams often run both rather than picking one. ASan is compile-time instrumentation — you add -fsanitize=address to your build and the compiler inserts the checks directly into generated code — which brings overhead down to roughly 2x rather than Valgrind's 10-50x, but it requires rebuilding every translation unit you want checked, including in some cases third-party libraries. Valgrind needs no recompilation at all: it works on whatever binary you already have, including closed-source components or code you can't rebuild, which makes it useful for triaging a crash in a shipped artifact. In practice, ASan's lower overhead makes it a better fit for routine CI runs on every pull request, while Valgrind's ability to instrument unmodified binaries makes it the tool of choice for one-off deep investigations, legacy binaries, or environments where recompiling with sanitizer flags isn't practical.
Where does this fit relative to static analysis?
Memcheck and other dynamic tools only see the code paths your test suite actually exercises at runtime, which is the structural limit every dynamic analysis tool shares — a use-after-free on an error-handling branch nobody tests simply never fires, and Valgrind reports nothing wrong. Static analyzers for C and C++, like Clang Static Analyzer or Coverity, instead walk the source without executing it, which lets them flag a suspicious free() on every theoretical path, at the cost of more false positives from paths that can't actually occur. The two approaches are complementary rather than substitutes: a mature C codebase typically runs static analysis on every commit to catch what's reachable by inspection, and reserves Memcheck or ASan runs for what only shows up when the code actually executes with real inputs. Safeguard's own SAST engine, as of this writing, is scoped to JavaScript/TypeScript, Python, and Java rather than native C or C++ source — teams working in C still need a dedicated tool like Valgrind, ASan, or a C/C++-focused static analyzer in their pipeline, since supply-chain reachability analysis for C/C++ dependencies is a different capability from finding memory bugs in code you wrote yourself.