---
title: "The Patch Is a Disclosure. The Bug Is in the Delta."
description: "Patch diffing turns the vendor's fix into a map to the vulnerability. The workflow is not 'find the bug' — it is 'find the change, read it backwards, and hunt the same class everywhere else.'"
date: 2026-05-28
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/posts/patch-diffing-methodology/
---

## Patch diffing in 60 seconds

A security patch is the vendor writing down, in public, exactly what was wrong. They do not
intend it as a disclosure. They intend it as a fix. But the two artifacts are the same bytes,
and anyone who can read the difference between the patched and the unpatched binary has been
handed the vulnerability.

Patch diffing is the practice of reading that difference backwards. The vendor ships the
answer; the work is reconstructing the question.

## Why the fix is the best starting point

Most vulnerability research starts from a black box: a product, a protocol, a file format,
and no idea where the bug is. That is the expensive way to work, and it is why so much of it
fails to find anything.

A patch inverts the problem. Somewhere inside it is a specific code path that changed for a
reason, and that reason is a bug. You do not have to find the bug. You have to find the
*change*, and the change is legible — a function that grew a check, a length that changed, a
call that moved.

The vendor has done the hard half of the research and published it. Diffing is how you
collect the other half.

## The workflow is five steps, and the middle one is everything

<figure class="diagram">
<svg viewBox="0 0 700 236" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The patch diffing workflow as a five-stage pipeline: collect both binaries, diff them, isolate the changed function, read the change backwards to reconstruct the root cause, then hunt variants of the same class elsewhere.">
<defs>
<marker id="pd-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow" /></marker>
</defs>
<rect x="0" y="30" width="120" height="52" rx="8" class="dg-box" />
<text x="14" y="50" class="dg-label">two builds</text>
<text x="14" y="67" class="dg-muted">vuln + patched</text>
<line x1="122" y1="56" x2="158" y2="56" class="dg-line" marker-end="url(#pd-a)" />
<rect x="162" y="30" width="120" height="52" rx="8" class="dg-box" />
<text x="176" y="50" class="dg-label">diff</text>
<text x="176" y="67" class="dg-muted dg-compact">find the change</text>
<line x1="284" y1="56" x2="320" y2="56" class="dg-line" marker-end="url(#pd-a)" />
<rect x="324" y="30" width="130" height="52" rx="8" class="dg-box-accent" />
<text x="338" y="50" class="dg-label">isolate function</text>
<text x="338" y="67" class="dg-accent">THE WHOLE GAME</text>
<line x1="456" y1="56" x2="492" y2="56" class="dg-line" marker-end="url(#pd-a)" />
<rect x="496" y="30" width="140" height="52" rx="8" class="dg-box" />
<text x="510" y="50" class="dg-label">read backwards</text>
<text x="510" y="67" class="dg-muted dg-compact">recover root cause</text>
<line x1="566" y1="84" x2="566" y2="118" class="dg-line" marker-end="url(#pd-a)" />
<rect x="496" y="122" width="140" height="52" rx="8" class="dg-box-accent" />
<text x="510" y="142" class="dg-label">variant hunt</text>
<text x="510" y="159" class="dg-accent">SAME BUG CLASS</text>
<line x1="0" y1="120" x2="700" y2="120" class="dg-line dg-dash" />
<text x="0" y="200" class="dg-crit">THE INVERSION</text>
<text x="86" y="200" class="dg-mono">you are not looking for a bug — you are looking for a change,</text>
<text x="86" y="218" class="dg-mono">and the change is the bug, spelled out by the person who fixed it</text>
</svg>
<figcaption>The first two steps are mechanical. The value is in step three: a patch rarely touches many functions, and the one it touched is where the vulnerability is, whether or not the changelog admits it.</figcaption>
</figure>

**One: get both builds.** The patched and the unpatched binary, firmware image, or package.
This is the step that actually requires effort — the vendor does not archive the vulnerable
version, and older firmware has a habit of vanishing from the download page. Mirrors, package
registries, and update caches usually have it.

**Two: diff them.** The mechanical pass. Function-level diffing matters more than byte-level,
because a compiler change reorders everything and hides the one real change in a sea of noise:

```
$ radiff2 -C vuln.bin patched.bin
```

`-C` produces a code-level diff that survives address and register renames. For heavier work,
Diaphora (IDA) and Ghidra's version tracking produce a function-level map between the two
builds, which is what you actually want — the answer to "which function changed, and by how
much".

**Three: isolate the changed function.** This is the whole game. A typical security patch
changes a handful of functions, and most of those changes are trivial. The one that matters
is the one that grew a length check, or a bounds comparison, or a validation call that was
not there before.

**Four: read the change backwards.** The fix tells you the bug. A new `if (len < 0)` means
the length was attacker-controlled. A new null check means a null was reachable. A moved
`memcpy` with a smaller size means an overflow was possible. Every fix is a sentence about
the vulnerability it closes, and reading it backwards is the actual skill.

**Five: hunt the variant.** Once you understand the root cause, you stop being interested in
this one binary. The same class of bug is elsewhere — in the same function's siblings, in the
same vendor's other products, in every codebase that solved the same problem the same way.

## Reading the delta backwards

The specific tells are small and consistent across every vendor:

- **A size that changed.** `memcpy(dst, src, n)` becomes `memcpy(dst, src, min(n, limit))`.
  The pre-patch value of `n` was the vulnerability, and the question is only where it came
  from.
- **A check that appeared.** A comparison before an indexing operation, a range check before
  a write, a `strnlen` where a `strlen` used to be. The check is the patch; the thing it now
  rejects is the bug.
- **A call that moved.** An authentication or authorisation call that used to run after the
  sensitive operation now runs before it. That ordering change is a missing-authorisation
  finding, written as a fix.

None of this requires understanding the whole binary. It requires understanding the delta,
and the delta is small. The vendor did the hard part; the diff is a map, and maps are short.

## From one patch to many bugs

The variant hunt is where patch diffing stops being a 1-day activity and becomes real
research. The root cause is a class, not a location.

If the fix was "the length field was never validated before the copy", the same field, in
the same format, parsed by the same library, is copied somewhere else in the same binary —
and in the vendor's other products, and in every third-party implementation of the same
protocol. One patch, read once, yields a list of places to look that no fuzzer would have
prioritised, because the researcher now knows exactly what shape the bug takes.

That is the difference between a CVE and a line of research. The patch gave you a single
instance. The root cause gives you a class, and the class is where the follow-up findings
are.

## Evidence matrix

| Signal | What it proves | Negative control | Defender verification |
| --- | --- | --- | --- |
| Old and fixed artifacts have recorded versions and hashes | The comparison is reproducible and not a packaging mismatch | Diff two identical builds and expect no security-relevant delta | Preserve provenance, symbols, compiler metadata, and hashes with the analysis |
| A small changed function introduces a new validation or bounds check | The patch points to the condition the vendor considered unsafe | Exercise an input that never reaches the changed branch | Map the binary delta back to source-level behavior or an equivalent control-flow trace |
| Old build accepts the minimal trigger and fixed build rejects it | The delta changes security behavior, not only implementation shape | Run a nearby valid input successfully on both versions | Test both versions in the same harness and record response, crash, or state difference |
| Sibling call sites repeat the pre-patch pattern | The root cause may be a bug class rather than one CVE instance | Confirm patched/guarded siblings do not match the trigger | Search semantic variants and triage each with the same positive and negative controls |

## The pattern I keep seeing

Patch diffing gets treated as a vendor-adjacent, slightly disreputable practice — as though
reading a published change were somehow cheating at vulnerability research. It is the
opposite. The vendor published the change precisely so that people would apply it, and any
competent attacker is reading it anyway. The only party that loses when researchers do not
diff is the defence, which never learns the root cause and therefore never fixes the class.

The other thing that keeps coming up is how much of this is just version discipline. Most
organisations cannot tell you what binary version they are running, which means they cannot
tell you which vulnerabilities they have already been handed a map to. The patch is public.
The delta is readable. The gap is not a shortage of information — it is that nobody is
reading.

## What to hand the defenders

**The root cause, not the CVE.** A report that says "apply the patch for CVE-XXXX" closes
one instance. A report that says "this length field is trusted at three call sites, only one
of which the patch fixed" closes a class. The diff gives you the second report for free, and
it is the one that changes the outcome.

**The affected version list.** Patch diffing depends on knowing which build you are looking
at, and most teams cannot produce that list. Establishing it is the prerequisite for every
other finding in this area.

**The variant list.** Where else does the same pattern live? Name the siblings, the other
products, the third-party libraries. That is the part that turns a vendor's fix into a
durable improvement in your own posture, instead of a one-off ticket.

The patch was never just a fix. It was the vendor telling you, in the clearest terms it has,
exactly what to go looking for.