The Hidden Cost of Running Sanitizers One at a Time
CombiSan is the subject of this series: a fuzzing-oriented sanitizer designed to bring the detection capabilities of AddressSanitizer (ASan), MemorySanitizer (MSan), and UndefinedBehaviorSanitizer (UBSan) into one execution. The opening question is not whether any of those tools is useful. It is why a team can run all three and still obtain a slower, less uniform picture of what its fuzzing campaign has exercised. The answer provides a practical way to judge CombiSan: look at what each detector can see, which inputs receive that view, and what happens when the first finding ends an execution.
That shift in perspective matters because “we fuzz with three sanitizers” can describe two very different arrangements. In one, every generated input is observed by a combined detector. In the other, one stream of inputs is run under ASan and UBSan, another under MSan, and the results are later placed side by side. The second arrangement may be necessary when the tools cannot coexist, but it is not equivalent coverage. It spends more execution budget while allowing the fuzzer's randomness and the detectors' stopping rules to decide which evidence survives.
A sanitizer is a lens, not a verdict
Fuzzing supplies a large and changing collection of inputs; a sanitizer instruments the program so that particular runtime conditions become visible when one of those inputs triggers them. That division of labor is easy to blur. A sanitizer does not inspect every possible input, prove that an unreported path is safe, or replace the fuzzer's ability to reach a path. It makes a chosen class of failure observable on the executions that actually occur.
The classes in the CombiSan paper are related, but they are not interchangeable. An addressability error means that a program accesses memory outside an object's valid lifetime or bounds. A write beyond an array and a read through a pointer after its object has been freed are both serious memory errors, but they arise at different points in an object's life. ASan makes these failures visible by surrounding objects with redzones and delaying the reuse of freed memory through a quarantine. Loads and stores can then be checked against the addressability state the runtime maintains.
MSan asks a different question: not simply whether a byte can be addressed, but whether an uninitialized value reaches a meaningful use. An uninitialized value that is overwritten before it affects a branch may not be the same defect as an uninitialized value that chooses a control-flow path, becomes an address, or is passed to an operation that expects initialized data. MSan follows the value's initialization state through the program. That precision is useful, but it creates a demanding build condition: the program and its important dependencies need compatible instrumentation, or missing state can produce false reports. The Clang documentation describes a typical slowdown of about three times for MSan and emphasizes the difficulty of instrumenting external code.
UBSan covers yet another family of checks. Depending on the selected options, it can report events such as signed integer overflow, invalid shifts, null or misaligned pointer use, and certain out-of-bounds operations or invalid indirect calls. Most UBSan checks can report and continue, while selected checks can be configured to exit or trap. The distinction matters because the same detector can behave as a continuing observer or as a stop signal, depending on the runtime policy and the target's behavior.
These three lenses therefore differ in both subject and timing. ASan is concerned with whether an access is addressable. MSan follows whether a value has been initialized when it reaches a sensitive use. UBSan checks whether a selected operation violates a language or runtime rule. Their metadata, compiler instrumentation, dependencies, and responses to a finding are not identical. A combined fuzzing design has to preserve those differences instead of flattening them into a generic “sanitized” state.
The consequence is a useful rule of thumb: a clean run means only that the configured detector did not observe its configured conditions on that input. It does not mean that the same input would be clean under every detector, or that another detector would have reached the code that follows the first failure. CombiSan matters because it treats this difference between local detection and campaign-wide observation as an engineering problem.
Why the second run changes the evidence
Consider a parser that receives one malformed input. Early in its execution, the input triggers a condition that ASan or UBSan reports. A conventional sanitizer integration exits at that point, which is deliberate. The ASan documentation explains that stopping at the first memory error keeps the generated code smaller and faster and avoids interpreting a process after memory corruption has put it in an inconsistent state. For a single diagnostic, that is a sensible boundary.
For a multi-detector fuzzing campaign, however, the same boundary can hide what happens later in the input's path. The parser might have reached a different undefined operation, or a later memory access might have exposed a separate bug, if execution had continued. The first report does not make the later condition disappear from the source; it makes it absent from the evidence produced by that execution. If the detector that stops first is not the one that would have classified the most important failure, the report also shapes what the fuzzer saves and how the input is triaged.
Running a second sanitizer job does not fully repair that loss. The second job starts from a related corpus, but a fuzzer is stochastic: mutations, scheduling, coverage feedback, and execution timing can lead two campaigns down different paths. An input that reaches a deep state under ASan and UBSan may never be generated under MSan. Conversely, an input that exposes an uninitialized-memory problem under MSan may not appear in the other job. The relevant detector can therefore be absent precisely when a valuable input is produced.
This is the hidden cost of sequential testing. The obvious cost is compute. If a team wants to give ASan-class, MSan-class, and UBSan-class defects comparable exposure, it may need one budget of executions for the addressability-plus-UB configuration and another for MSan. The paper's later performance comparison models that as two sets of inputs rather than one. A combined run still has instrumentation overhead, but each generated input can be examined through the intended collection of lenses.
The less obvious cost is uneven observation. Two jobs do not merely divide the same experiment into convenient batches; they sample different executions. Results from the jobs can still be valuable, especially when a tool cannot be deployed with the others or when a finding needs independent confirmation. But their union is not the same as having every detector present for every input. It is a coverage decision, not just a scheduling decision.
Report policy creates a second form of unevenness. A target may execute the same undefined operation for nearly every input. If that condition is treated as a fresh crash every time, the fuzzer can spend its attention rediscovering the same shallow event instead of exploring deeper behavior. UBSan's documented recovery controls show that continuation is possible for many checks, but the right policy depends on the check and the target. The CombiSan paper gives a concrete example in Ghostscript, where a recurring function-pointer issue caused every input to trigger undefined behavior under the tested UBSan setup. Its approach records the first occurrence while allowing known repetitions to complete, so the campaign can continue searching.
The operational setting makes these choices consequential. OSS-Fuzz's project guide lists address, memory, and undefined as available sanitizer configurations. It describes address and undefined as the default supported list, while memory is recommended but not enabled by default because uninstrumented system dependencies can create false positives. That is not a judgment that MSan is unimportant. It is evidence that detector coverage depends on build boundaries and maintenance effort as much as on the detector's theoretical scope.
The diagram is not saying that separate runs are useless or that every process can safely continue after a memory error. It shows why the unit of comparison must be the campaign's evidence. A detector can be fast and accurate on an input while the overall arrangement remains incomplete because another detector never sees that input or because the process ends before its later behavior is observable.
CombiSan's wager: shared observation
CombiSan's wager is that the campaign should not have to choose between incompatible sanitizer runs when the goal is to expose these three broad classes of defect. The paper presents it as a compiler-based sanitizer that combines ASan-class addressability detection, MSan-class uninitialized-memory detection, and UBSan instrumentation in a fuzzing-oriented setup. Its contribution is therefore larger than putting three names in a build command. It changes the unit of observation from “one sanitizer configuration per run” to “one input examined by a coordinated detector.”
That coordination has two requirements. First, the runtime must represent the relevant memory states without asking ASan and MSan to maintain conflicting shadow memories. Second, the system must collect enough information to keep one violation from ending the input before other violations are recorded. CombiSan addresses those requirements with a unified memory-state design and deferred analysis; the details of the shared state, allocation lifecycle, and confirmation of uninitialized-memory candidates are the next technical questions to examine.
The opening claim should still be kept narrow. CombiSan does not establish that every sanitizer can be merged, that every bug class is covered, or that a single combined run is always faster in every environment. The paper evaluates a specific prototype, target set, fuzzing mode, and comparison model. Its value is that it makes a hidden systems question testable: do shared observation and deliberate report handling produce more useful evidence than isolated runs whose results are collected only afterward?
That question gives us the vocabulary needed for the rest of the series. We will distinguish what a detector observes from what a fuzzer reaches, what a runtime records from what it treats as a campaign-stopping event, and what a benchmark measures from what it leaves outside the boundary. Before judging CombiSan's performance or its 81 newly found bugs, we first need to see exactly what ASan, MSan, and UBSan each ask the program to remember—and why those memories do not naturally fit together.