The Bug That the Other Run Never Sees
CombiSan is designed around a problem that only appears when a fuzzing input is treated as more than a crash candidate: one execution can contain several useful signals, but a conventional sanitizer may stop after the first. This chapter follows the life of an input and then the life of a campaign to show how that stopping rule, recurring undefined behavior, and separate stochastic runs change the evidence a team collects. The goal is to distinguish a detector's per-execution behavior from the campaign-level question of whether every valuable input received the right set of observations.
One input, multiple endings
A fuzzing input begins as bytes in a queue and becomes meaningful only when the target program interprets them. The instrumented target may allocate memory, parse fields, perform arithmetic, call a library, and eventually return. Sanitizers sit inside that path, checking selected operations as they occur. If one check fires early, the rest of the path is not a hypothetical detail: it is behavior that this execution will no longer reveal.
Suppose a malformed input sends a parser through two problematic operations. The first is an out-of-bounds access; the second, reached only if execution continues, is an integer overflow or an invalid use of a value that was never initialized. Under ordinary ASan behavior, the first addressability error produces a report and the process exits. That behavior is intentional. After memory corruption, the program's state may no longer be reliable enough for later reports to be interpreted cleanly, and the ASan documentation explains that early exit also permits smaller, faster generated code.
The consequence is a form of bug shadowing. The first error is visible because it controls the process boundary. The second error may be just as real, but it is absent from the record for this input. A different sanitizer configuration can expose it only if the same input reaches the same point without being stopped earlier. If the relevant operation is checked only by UBSan, an ASan run may never report it; if the input is tested under UBSan first and that configuration exits, an addressability error later in the path may remain unseen instead.
This does not mean that every later report would be useful. After a genuine memory corruption, continuing an arbitrary program can produce consequences that are difficult to trust. That is why CombiSan does not simply turn every sanitizer into a permissive logger. Its approach records the details of violations while the input runs, then lets the fuzzer decide what to do after the test case completes. Addressability failures are treated as fatal for the program state after collection, because the process may be unstable. Collection and action are separated, but seriousness is not erased.
Undefined behavior creates a different stopping problem. A bug that appears once can be valuable as a new discovery. The same bug appearing on every subsequent input can become a gate that prevents the fuzzer from exploring anything beyond it. UBSan supports recovery for most checks, and its runtime can be configured to exit or trap for selected checks. Yet the right setting depends on the target and the purpose of the run. A report that is allowed to recur without a policy for deduplication can still dominate the campaign's attention.
The CombiSan paper's Ghostscript example makes that distinction concrete. A small mismatch in a function-pointer type caused every input in the tested setup to trigger undefined behavior. If each occurrence were treated as a fresh stopping event, the fuzzer would repeatedly rediscover the same condition at the same stage. CombiSan instead treats a previously unseen UB as a crash-like event, allowing it to be recorded and processed, while known repetitions can complete. That policy does not declare the function-pointer issue safe. It changes the transition after the first observation from “stop all exploration here” to “remember this boundary and continue looking for other behavior.”
The difference between collection and action can be described as a small state machine. While the input is running, the target is in an observation state: it can accumulate addressability failures, undefined-behavior reports, and candidate uninitialized loads. At completion, the fuzzer receives the set of observations and enters an analysis state. It can confirm a candidate, deduplicate a known site, classify a new UB, or reset after a serious memory error. Only then does it return to the queue and choose the next input.
That sequence matters because a report is not the same thing as a fuzzer decision. An immediate crash couples them: the detector reports, the process ends, and the fuzzer infers what happened from that termination. CombiSan decouples them long enough to preserve more information from one test case. The trade-off is that its runtime must retain enough location and context to tell violations apart, and the target must be safe enough for the intended continuation window. The paper presents this as a fuzzing-specific workflow, not as a general license to run corrupted programs indefinitely.
The sequence has two paths because the distinction is about control ownership. In the separate path, the sanitizer owns the point at which the target stops. In the combined path, the runtime records events and the fuzzer owns post-test analysis. The combined path still has to reset or classify the input when appropriate; it simply postpones that decision until the input has yielded the observations it can safely produce.
From one input to a campaign
The single-input example explains why running a second sanitizer is not a perfect repair. The larger problem is that the second run is not necessarily a replay of the first. Fuzzers are stochastic systems. They mutate inputs, select queue entries, respond to coverage, and spend execution time according to changing state. Two jobs that begin with related seeds can quickly develop different queues and different paths.
Imagine that the ASan-plus-UBSan job generates a rare input that reaches a deep parser state. That input might expose an uninitialized-memory issue, but the MSan job will never know unless it receives and executes the same test case. The reverse is also possible: an MSan run may discover an input that reaches an addressability failure, while the ASan job explores a different mutation instead. A later replay can establish whether a supplied input triggers a given detector, but replay does not change what the original campaign observed or which inputs it chose to mutate next.
This is why the paper calls sequential sanitizer execution heterogeneous. The jobs are not simply two cameras pointed at the same experiment at different times. Their random choices alter the experiment itself. Coverage feedback from one configuration can favor inputs that are not favored by another; a report that terminates one job can prevent that job from learning from deeper behavior; and a bug may be generated while the detector capable of recognizing it is absent.
A second problem is stability. If a sanitizer configuration has frequent stopping events, its fuzzer may spend less time producing varied executions than its nominal run duration suggests. An input that ends immediately supplies less opportunity for the target to reach later code and less opportunity for the fuzzer to learn from that execution. Repeated reports can therefore shape the queue toward the same shallow behavior, while a configuration that continues farther may generate a different set of candidates. Stability is not a cosmetic metric: it changes the feedback loop through which execution results become future inputs.
That distinction is important for interpreting a bug count. If one configuration finds fewer bugs, at least four explanations are possible: the detector may not recognize a real defect; the fuzzer may not generate the triggering input; an earlier report may stop the input; or instability may reduce useful exploration. A campaign that runs sanitizers one by one makes those causes harder to separate because each configuration changes both the detector and the input population. The paper's broader point is not that every difference between runs proves a detector weakness, but that detector behavior and input generation must be measured together before a count can be explained.
The compute arithmetic is simpler but still easy to misread. Suppose a team wants to expose a target to N conceptual inputs for each of three sanitizer classes. If ASan and UBSan can run together but MSan cannot, the sequential arrangement needs N inputs under the first configuration and another N under MSan: two sets of executions. A unified configuration can examine the same N inputs under all three capabilities, although each individual execution may be slower than an unsanitized run. The advantage is not that the combined runtime has no cost. It is that the campaign does not pay for a second stochastic exploration merely to attach a different detector to a different population of inputs.
This is also why per-execution throughput is not enough to decide between the arrangements. A combined runtime can lose some executions per second and still complete the intended multi-class campaign sooner. Conversely, a nominally fast configuration can be a poor campaign choice if it repeatedly stops at a known condition or leaves a crucial bug class outside its observation. Total useful exploration depends on both the cost of each execution and the fraction of generated inputs that receive the needed detector view.
There are reasonable cases for separate runs. A sanitizer may require dependency instrumentation that a target cannot provide, or a project may need to isolate a detector while it resolves a configuration or report-policy problem. Separate campaigns can also provide independent confirmation and can isolate one detector when its behavior is the object of study. The claim is not that sequential testing is invalid. It is that the team should recognize the price: different runs produce different evidence, and their combined result is not automatically equivalent to simultaneous observation.
OSS-Fuzz's deployment guidance shows why this price appears in real systems. The project guide lists address, memory, and undefined as available configurations, but says that address and undefined are the default supported set. Memory is recommended yet requires manual opt-in because the project's needed runtime dependencies must be built with MSan-compatible instrumentation to avoid false positives from uninstrumented code. In a continuous service, that requirement affects how many projects can practically receive the MSan lens. A campaign that omits a detector because it cannot cross the build boundary has a coverage gap before the fuzzer begins mutating inputs.
CombiSan's proposed response is therefore about alignment. It tries to make the target, the sanitizer state, the error policy, and the fuzzer's input stream share one execution. Alignment does not remove the ordinary fuzzing coverage problem: a detector still cannot report a bug on a path no input reaches. It does make the interpretation of a generated input more uniform, because the same test case can carry evidence about several classes before the fuzzer decides how to classify or reset it.
The remaining architectural question is how that alignment can be implemented without maintaining incompatible shadow memories or paying the full cost of every detector on every event. The answer begins with the state a single byte must carry: whether the address is allowed, whether its value has been initialized, and how those two facts should change when the program allocates, writes, reads, and frees memory.