Shrink the codebase to the code an attacker can reach
The whole codebase is not the search space. The search space is the code that handles bytes somebody else chose. Start by listing every place input crosses a trust boundary: network protocol handlers, file format parsers, decompressors, deserialisers, image and font decoders, configuration loaders that accept user-supplied files, and any IPC endpoint a less privileged process can talk to.
Then rank them. A parser for a binary format with nested length fields deserves more attention than a JSON config loader behind authentication. Ask three questions of each one. Does it compute sizes or offsets from input? Is it old, and was it written before the team cared about fuzzing? Does it run in a process with privileges worth stealing? The two or three modules that answer yes to all three are where you spend the first day.
This step is what an interviewer is listening for, because it is the step that stops the task being impossible. A candidate who opens with "I would grep for strcpy" has skipped it.
Grep for the arithmetic, not only the dangerous function
Grepping does help, as long as it is scoped to the modules you ranked. The classic call names, such as memcpy, strcpy, sprintf, strcat and alloca, give you a list of risky call sites. The real question at each site is where its length came from.
Most memory-safety bugs in mature code are not a bare strcpy. They are a length read from a header and trusted, a count * size that overflows before it reaches malloc, a signed length compared against an unsigned bound, or an off-by-one in a loop that writes a terminator. So search for the arithmetic feeding the copies too: multiplication inside allocation calls, casts between signed and unsigned types near length checks, and pointer arithmetic using a value parsed from input. In Rust, the equivalent grep target is unsafe blocks and FFI boundaries. In Java or Python, it is the native extensions and JNI code underneath, where the language's own safety stops applying.
Make the bug announce itself with sanitisers
Here is the fact that changes the whole approach. A small out-of-bounds write usually does not crash. It overwrites a neighbouring heap object, the program carries on, and the damage shows up much later and somewhere else, if it shows up at all. Running the test suite and seeing it pass tells you almost nothing.
AddressSanitizer fixes that. Building with -fsanitize=address in Clang or GCC adds shadow memory and red zones around allocations. An overflow, a use-after-free or a double free then stops the program at the faulting access, with a stack trace. For a use-after-free it also prints where the memory was allocated and where it was freed, which is often enough to explain the bug without a debugger. Clang's documentation puts the typical slowdown at about 2x, so you can run a whole integration suite with it. Add -fsanitize=undefined for signed overflow, invalid shifts and misaligned access. Clang's MemorySanitizer covers reads of uninitialised memory, but it cannot be combined with AddressSanitizer in the same build.
Running the existing test suite under sanitisers is the cheapest first move, and it sometimes finds the bug before you have written anything.
Point fuzzing at the ranked entry points
Coverage-guided fuzzing is how you search input space faster than you could ever read code. You write a small harness that hands the fuzzer's bytes to one entry point. The fuzzer mutates inputs, keeps the ones that reach new code, and the sanitiser turns each bad access it detects into a reproducible crash file.
#include <stddef.h>
#include <stdint.h>
#include
{
(img_parse(&img, data, size) == ) {
img_free(&img);
}
;
}
Build it with clang -g -O1 -fsanitize=fuzzer,address,undefined. Seed the corpus with real sample files, because a fuzzer starting from valid inputs gets past the magic-number checks in minutes rather than days. AFL++ is an alternative engine that does the same job. Harness choice is the judgement call. A harness that calls a deep helper with a pre-built struct skips the header parsing where the length gets trusted, and so it skips the bug.
One thing can silently defeat the sanitiser. If the code allocates from its own pool, ASan sees one large allocation, and an overflow from one object into its neighbour inside that pool goes unreported. Either build with the pool switched to plain malloc, or poison the free regions by hand using the manual poisoning interface in sanitizer/asan_interface.h.
Use history and crashes to close the distance
Git history narrows things from a different direction. Code that changed recently in a parser is more suspicious than code untouched for a decade. git log -S finds the commits that added or removed a given expression, such as a length check. If you have a crashing input and a known good release, git bisect with the sanitised build as the test finds the commit that introduced the bug.
Crash triage is the last step. Deduplicate fuzzer crashes by the top frames of the stack, because a hundred crash files are usually three bugs. Minimise each input, read the sanitiser report, and fix the cause at the point where the bad length was accepted, not at the copy that tripped over it.
Starting from the dangerous call instead of the untrusted byte
The weak answer treats this as a reading problem and tries to speed up the reading: grep every memcpy, open each one, and decide whether it looks safe. On a large codebase that produces thousands of sites, most of them harmless, with no ranking and no way of knowing when to stop.
The strong answer turns it round. It starts from the bytes an attacker controls, follows them to the first place they become a length or an index, and puts instrumentation and a fuzzer on exactly that path. It also says out loud that a clean run proves nothing without a sanitiser, because the bug you are hunting is usually silent.
You cannot read the codebase, but you can read its trust boundaries. The bugs worth finding first sit on the paths an attacker's bytes reach, most often where those bytes become a size or an index, and sanitisers plus a fuzzer aimed at that point will find it faster than any amount of reading.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).