Add the CPU-vendor divergence reference

references/cpu-vendor-divergence.md records where the two x86-64 implementations
diverge: undefined results, vendor-string dispatch, feature-gated decoding,
system-instruction fault timing, and model-specific cost, each stated with what
it changes for reading a binary or writing a specification.

Register it in the routing index, both symptom tables and the route table; add
the coverage row (inferred, no second-vendor host), the route-inventory row
(argued) and the evidence note that every measurement here came from one
implementation.

Append P38 for an undefined result recorded as a defined one, and point
assembly-idioms and hypervisor-observation at the new file.
This commit is contained in:
root
2026-10-24 18:49:46 +08:00
parent 9fd8422d8f
commit c6ef1860cd
9 changed files with 247 additions and 1 deletions
+3
View File
@@ -145,6 +145,7 @@ Load the named file **before your next command**, not after your next three atte
| The file checksum no longer matches and something now refuses the binary | `references/patching.md` |
| Your signature-based patch worked on one version and misses on the next | `references/aob-and-signatures.md` |
| Two supposedly identical builds behave differently | `references/pe-diff-and-versions.md` |
| A check passes here and fails on the machine the deliverable is for, with the same OS build | `references/cpu-vendor-divergence.md` |
| The target is a .NET assembly and hex-patching it does nothing | `references/managed-dotnet.md` |
| A .NET assembly decompiles, but the names are gone or the C# does not add up | `references/dotnet-decompilation.md` |
| A Unity game's `global-metadata.dat` is missing, empty or unreadable beside its `GameAssembly.dll` | `references/unity-il2cpp.md` |
@@ -165,6 +166,7 @@ Load the named file **before your next command**, not after your next three atte
| A detour or code cave crashes its callee, or the arguments arrive at wrong offsets | `references/assembly-idioms.md` |
| A 32-bit target's licence file or registry key is absent from the path the documentation gives | `references/assembly-idioms.md` |
| A prologue does not look like the compiler the headers named | `references/assembly-idioms.md` |
| A routine compares the CPUID vendor string, or branches on a flag the manual calls undefined | `references/cpu-vendor-divergence.md` |
| You are about to conclude "it must be server-side" | `references/network-activated-licensing.md` |
| You cannot tell whether a failure is your patch or the target's own behaviour | `references/verification.md` |
| You are about to write off a route as impossible | `references/protectors-and-packers.md` |
@@ -220,6 +222,7 @@ is the most common way an hour disappears.
| A driver or kernel component | Kernel route; user-mode tooling stops at the boundary | `references/kernel-and-drivers.md` |
| Something that detects every hook you install | Hardware observation, and only if the probe says this host supports it | `references/hardware-tracing.md` |
| A virtualised or flattened routine | Symbolic/emulation route before more manual reading | `references/symbolic-execution.md` |
| Code that dispatches on CPU vendor or feature level | Pin the decoder's target CPU and read both paths; a vendor compare is not a feature test | `references/cpu-vendor-divergence.md` |
| Behaviour that is mostly network | Capture and classify authority | `references/network-activated-licensing.md` |
**If the target does not match any row, stop and classify before choosing a branch.** A method that
@@ -231,6 +231,9 @@ What this changes during analysis:
confirmed by observation, not by reading (`references/dynamic-debugging.md`).
- **No disassembler is shipped here.** The reading is done with whatever the host provides, and
`scripts/hwprobe.py` reports whether that is anything at all.
- **An idiom is not a guarantee.** The same bytes can mean two instructions and the same instruction
can leave an undefined result, so a behaviour that differs between processor implementations is not
an idiom — it is `references/cpu-vendor-divergence.md`.
- **A reconstructed frame is a hypothesis.** The listing gives the shape a compiler intended; the
unwinder gives the one the processor used. Where they disagree, the run is authoritative.
- **Conventions describe calls the compiler emitted.** A stub written by hand, a callback invoked by
@@ -42,6 +42,7 @@ named gap.
|---|---|---|---|
| Static PE structure analysis | `observed` for the reader; `unverified` per target | the shipped reader (`scripts/peutil.py`) parses the DOS header and stub, compiler fingerprint, NT and optional headers, data directories, sections, both offset directions, imports, delay imports, exports, resources, base relocations, TLS callbacks, debug directory and CodeView/PDB, load configuration, the certificate table location, a recomputed file checksum and per-section entropy (`observed`: a summary run over a local sample returned architecture, signature state, declared-versus-computed checksum agreement and the populated directory set) | a disassembler or decompiler for anything past the headers; a second reader when two tools disagree about one field |
| Instruction-level reading | `inferred` | the calling conventions, frame shapes and idiom meanings are documented architecture and toolchain behaviour, stated in `references/assembly-idioms.md` so a listing can be checked against them; no target was disassembled here | a disassembler or decompiler, and a build whose toolchain is known — the producer claim stays `inferred` until a second fingerprint agrees |
| CPU-vendor divergence | `inferred` | the differences in undefined results, system-instruction fault timing, feature-gated decoding and model-specific cost are stated from both vendors' specifications in `references/cpu-vendor-divergence.md`; no divergent behaviour was measured, because every run here came from one Intel host | a host of the other vendor (or a model pinned to one), and a target whose two paths can both be exercised |
| Byte patching | `observed` for the patcher's guards; `unverified` on any target | the shipped patcher (`scripts/pepatch.py`) opens the input read-only, writes a new file, refuses a length change without an explicit flag, refuses an ambiguous pattern, pins the expected bytes, re-parses its own output and re-reads every patched byte, and recomputes the checksum (`observed`: a dry run printed the site, the byte pair, the unproven-length note, and wrote no output file) | a debugger, to establish that the patched site is the site that executes |
| AOB pattern work | `observed` as a matching rule; `inferred` as a method | a three-byte pattern matched 64 sites and the tool refused the patch, naming three ways out (select an index, accept all, or extend the pattern with context); exact-once is the default (`observed`) | a disassembler, to argue that a pattern's identity is stable across the builds that matter |
| Dynamic debugging workflow | `unverified` | the route is described and a debugger exists on at least one host; no breakpoint log, stepping session or debugger transcript is recorded in this repository | an interactive debugger, and a target you are permitted to run |
@@ -26,6 +26,7 @@ becomes `run` only when someone runs it and records the output.
| Triage | first contact with an unknown binary: identity, build evidence, protective signals, surface | Python only | `run` |
| Toolchain fingerprinting | deciding which toolchain produced a binary, and what that implies for reading it | the binary; a disassembler only if the fingerprint is inconclusive | `run` for the header-level signals, `argued` for the toolchain-specific structure |
| Instruction-level reading | reading a routine's convention, frame and idiom so that a patch or a detour preserves what the callee expects | a disassembler or decompiler — the repository ships none | `argued` |
| CPU-vendor divergence reading | deciding whether an observed result is architectural or one implementation's choice, and reading a vendor-string dispatch | the binary; a disassembler with its target CPU pinned; a second-vendor host to confirm | `argued` |
| Byte patching | making a change that fits an existing instruction or constant slot | Python only, plus a copy to write to | `run` |
| Signature location | finding a site across builds rather than at one address | Python only; a second build for the comparison | `run` |
| Build comparison | locating what changed between two builds | two builds | `run` |
@@ -0,0 +1,215 @@
# CPU vendor divergence: what x86-64 promises, and what one implementation decides
Goal: keep a result measured on one processor from being written down as a property of x86-64, and
recognise the code that selects its path by CPU vendor — because that code behaves differently on the
two vendors that run Windows.
## When to load this
Load this when a check passes on the analysis machine and fails on the machine the deliverable is for;
when a routine reads `cpuid`, compares a twelve-byte vendor string, or branches on a flag the manual
calls undefined; when a listing shows `syscall`/`sysret`, `rdrand`, `pause`, `lzcnt` or an AVX-512
form and the decoder and the CPU may not agree; when a target that died on one vendor stopped dying
after a firmware update; and before a behaviour observed on one host is written into a specification
another team will implement.
## The rule: x86-64 fixes the ABI, not the implementation
The Windows x64 ABI — registers, shadow space, alignment, unwind metadata — is one contract, and both
vendors implement it. What is not shared is each instruction's result where the manual says
*undefined*, the behaviour of system and privileged instructions outside their documented cases, and
the cost of an instruction.
`inferred`: the vendors' specifications agree on the encoding and disagree on what a program may rely
on. That disagreement is the subject of this file, not a defect in either implementation.
Every measurement in this repository comes from one Intel host (`references/evidence-summary.md`, host
facts), which is `observed` for that machine and `unverified` for the other vendor. "It did not
happen here" is therefore a statement about one implementation, not about the instruction.
## Undefined is a result you may not read
| Instruction | Documented contract | What it changes for analysis |
|---|---|---|
| `bsf`, `bsr` with a zero source | ZF=1 on both; Intel leaves the destination **undefined**, AMD documents it **unchanged** | Code that relies on the destination keeping its old value is guaranteed on AMD and merely usual on Intel. A specification that states the old value as the result is wrong on one vendor |
| `shl`/`shr`/`sar` with a count other than 1 | OF undefined on both | A branch on OF after a variable shift tests the implementation, not the program. The value observed is not a bug to reproduce |
| `imul`/`mul` | CF and OF defined; SF, ZF, AF and PF undefined | A check that reads ZF after a multiply is reading residue |
| `popfq` at CPL > 0 | privileged flag bits are not changed | An IOPL or IF write silently does nothing; a write that "took" is a different privilege level |
`inferred` from the instruction descriptions in both vendors' manuals. `unverified`: no divergent
value was measured here, and a particular part's residue is not a fact until it is measured on that
part.
Two consequences:
- **An analysis that explains a branch by an undefined flag has explained nothing.** The branch is
data-dependent on what the part left there. Reproduce it only on the part that produced it, and
record which part that was.
- **A protector can test the part this way.** A sequence that sets a flag, runs an instruction that
leaves that flag undefined, and branches on it is a family or vendor probe. The wrong move is to
"fix" the branch; the right move is to identify what it is testing.
## Vendor dispatch: the branch that never runs on your machine
`inferred`: `cpuid` with EAX=0 returns the highest basic leaf in EAX and the twelve-byte vendor string
in EBX, EDX, ECX — `GenuineIntel` or `AuthenticAMD`. Feature leaves live elsewhere: leaf 1, leaf 7 with
sub-leaves, and the extended leaves at `80000000H` and above.
The dispatch shape:
```
xor eax, eax
cpuid
cmp ebx, 'uneG' ; 'Genu', little-endian
jne other_vendor
```
What this changes:
- **A vendor string is not a feature.** A library or protector that dispatches on the vendor string
takes the other path on the analysis host, so the path you are reading may be the one that never
executes. Cross-reference the vendor compare with the feature leaves before concluding what the
code does.
- **A feature leaf is the portable test.** The same feature can be present or absent on either
vendor, so a correct implementation reads the leaf that defines the feature, not leaf 0.
- **A branch that never runs is the same mistake as an entry point that is never hit.** The breakpoint
is valid; the path it sits on is not the one this machine executes (`references/pitfalls.md`, P4).
Feature leaves used by the patterns below, `inferred` from the specifications — re-check each against
the revision you open, because a bit's meaning belongs to the family in front of you:
| Leaf | Bit | Feature |
|---|---|---|
| `CPUID.01H:ECX[31]` | hypervisor present | already reported by `scripts/hwprobe.py` |
| `CPUID.01H:ECX[5]` | VMX | Intel virtualisation |
| `CPUID.80000001H:ECX[2]` | SVM | AMD virtualisation |
| `CPUID.80000001H:ECX[0]` | LAHF/SAHF in 64-bit mode | absent on early Intel 64 parts |
| `CPUID.80000001H:ECX[5]` | LZCNT | AMD's ABM bit; also on later Intel parts |
| `CPUID.01H:ECX[30]` | RDRAND | hardware random |
| `CPUID.07H.0:EBX[18]` | RDSEED | seeded random |
| `CPUID.07H.0:EBX[16]` | AVX-512F | 512-bit vector baseline |
`python skills/exe-reverse/scripts/hwprobe.py` prints the vendor string, family/model/stepping, the
brand string and the raw leaves. It prints bits raw on purpose: the probe refuses to decode a meaning
it has not verified, and the reader is expected to open the specification for the part.
## The same bytes, two instructions
`inferred`: an instruction that reuses a legacy prefix is decoded by feature, not by encoding alone.
`F3 0F BD` is `lzcnt` where the feature is present and `rep bsr` (prefix ignored, different result)
where it is not. `F3 0F B8` is `popcnt`. `0F 0F` is 3DNow!, an AMD extension removed from current
parts. The XOP prefix `8F` carries AMD-only integer forms that a decoder for the other vendor reads as
a different instruction entirely.
Consequences:
- **A disassembler has a target-CPU setting, and it changes the listing.** A generic x86 target shows
`rep bsr` where the machine executes `lzcnt`. Record the decoder's processor setting with the
listing, the same way you record the tool's version.
- **An emulator is a third implementation.** It implements one CPU model and one decode map, and a
`cpuid` answer under emulation is that model's answer. A behaviour derived under an emulator
inherits the emulator's choices and is `unverified` for both vendors.
- **Feature-gated code has two bodies.** When one function is compiled for two feature levels, the
logic you want may exist in only one of them. Locate both before patching one.
## System instructions: privilege and fault timing
`inferred` from the vendor specifications and the public analysis of CVE-2012-0217:
| Case | Intel | AMD | What it changes |
|---|---|---|---|
| `sysenter`/`sysexit` in 64-bit mode | supported in IA-32e | invalid in 64-bit mode (`#UD`) | a reachable `0F 34`/`0F 35` in a 64-bit image carries an Intel-only assumption; the other vendor faults where the author's machine did not |
| `sysret` with a non-canonical RCX | `#GP` taken at CPL0 | privilege restored to CPL3 first, then `#GP` | the same return path faults at a different privilege level, so the handler that runs — and the stack it runs on — differs |
The CVE is the canonical case: a kernel that trusted the fault to arrive before the privilege change
was exploitable on the vendor where it arrives after. The lesson for an analysis is narrower than the
vulnerability: **a check whose failure mode is an exception has a vendor in its semantics.** Reading
the handler shows which case the code expects; "it faults" is not enough.
`unverified`: the cycle-level timing of `popfq`'s effect on TF and RF, and of a pipeline observing a
flag change. Do not build a detection or a patch on a timing claim that was not measured.
## Cost is implementation, and spin loops inherit it
`inferred`: `pause` is a hint whose latency is model-specific, not architectural. Intel's optimisation
guidance documents a change from roughly ten cycles on older parts to roughly 140 on Skylake and later;
AMD's parts use their own value. A spin loop tuned to one vendor's latency wastes time on the other, or
times out.
The same pattern applies to:
- **A `lock`-prefixed access that straddles a cache line.** It is architecturally legal and slow.
Recent Intel parts can report it as a split lock (`#AC`) when the platform enables the feature, and
whether a guest's policy enables it is a host property. A fault here is a policy finding, not a
target bug.
- **AVX-512 width and frequency.** Intel implements 512-bit execution with a frequency consequence on
some parts; AMD's Zen 4 executes it as two 256-bit halves without the same consequence. A throughput
number is a number about the part, and a timing check calibrated on one part behaves differently on
the other.
- **Topology.** Logical-processor counts, SMT numbering and cache sizes come from leaves that describe
the part in front of the program. A worker pool that hard-codes a topology runs correctly and slowly
on the other vendor — a performance bug that is really a portability one.
## Virtualisation and control-flow integrity: two instruction sets
`inferred` from the vendors' specifications:
| Mechanism | Intel | AMD |
|---|---|---|
| Enter and leave a guest | `vmxon`/`vmxoff`, `vmlaunch`/`vmresume`, `vmexit` | `vmrun`, `vmexit` |
| Control structure | VMCS, reached through `vmread`/`vmwrite` | VMCB, an ordinary memory structure read and written directly |
| Second-level translation | EPT | NPT |
| Shadow stack | CET (`endbr64`, `incssp`, `rdssp`, `saveprevssp`) | shadow stack with compatible instructions, different control MSRs |
What this changes:
- **Tooling is vendor-bound.** A debugger or hypervisor built on VT-x does not run on an AMD host.
That is a capability answer, not a target answer: `references/hypervisor-observation.md` describes the
layer, and which vendor's layer can be driven is a host fact.
- **A vendor-specific instruction in a driver is a portability statement.** Seeing `vmread` says which
vendor the driver was written for; it says nothing about whether its protection can be bypassed.
- **`endbr64` is a build-era marker.** Its presence says the image was built for a CET-enabled target,
not that the process is protected at run time; enabling is a policy decision above the image.
## What these divergences look like when they bite
`unverified` as field reports; the mechanisms above are what make each one possible.
| Shape | Check first |
|---|---|
| A launcher aborts on a subset of CPUs, and a firmware update makes it disappear | a hardware-random retry loop whose success flag never sets — measure CF in a control loop on that host before calling it anti-debug |
| A compute library runs several times slower on the other vendor at the same clock | a vendor-string dispatch that selects an older instruction path than the feature leaves justify |
| A game's worker threads collapse to half the cores on the other vendor | a hard-coded topology assumption in a third-party scheduler; the branch is the evidence and the patch site |
| A kernel driver bug-checks only on one vendor during a return to user mode | non-canonical `sysret` handling, and the privilege level at which the fault is taken |
| An analysis tool refuses to start on the other vendor | a VT-x-only implementation; read the vendor leaf before debugging the tool |
The method is the same in all five: **measure the part, not the platform.** Record the vendor string,
family, model, stepping, the microcode revision if the OS exposes it, the OS build and the hypervisor
state beside every timing, fault and feature result. A result without the part is not reproducible,
and a result reproduced on a different part is a different result.
## Before a behaviour goes into a specification
1. **Does the sentence assert a value for something the manual calls undefined?** Rewrite it as
undefined, and state what the implementation must not rely on.
2. **Is the behaviour conditional on a vendor or a feature?** Name the leaf and bit that select it, and
test the feature rather than the vendor.
3. **Was the observation made on the part the implementation will run on?** If not, the sentence is a
hypothesis, and it belongs in the specification as one.
4. **Which instruction set did the decoder assume?** Record it, because `F3 0F BD` is two instructions
and the listing shows one.
5. **Does the deliverable contain a timing constant?** Replace it with a measurement on the target
part, or with a bound that does not depend on one.
## What this file does not do
- **It does not rank the vendors.** Neither implementation is the reference; the specification is, and
where the specification says undefined, both are compliant.
- **It does not replace the manual for the part in front of you.** Leaf numbers and bit positions are
stable; what a model-specific bit means is not, and no table here substitutes for the revision you
open.
- **It does not measure the other vendor.** No AMD host was available, so every divergent behaviour is
`inferred` from documentation and `unverified` here. The first measurement on a second vendor
replaces the row; the row does not get to overrule the measurement.
- **It is not a portability guide for writing code.** The subject is reading a binary and writing down
what it is allowed to assume.
@@ -57,6 +57,7 @@ assume, and because a reader can compare their own `hwprobe.py` output against t
|---|---|---|
| OS | Windows 11, build 26100; the registry `ProductName` still reads "Windows 10" | read the build number, not the product name |
| CPU | family 6, model 154; `CPUID.07H.0:EBX[25]` set | the trace feature is reported by the CPU |
| CPU vendor | the vendor string `cpuid` returned was `GenuineIntel` | every measurement here is from one implementation; a result on this host is not a statement about the other vendor |
| Hypervisor | `CPUID.01H:ECX[31]` set and the platform reports a hypervisor present | a debug exception may be intercepted; hardware routes must be probed, not assumed |
| Debug registers | four (`DR0`–`DR3`) is the architectural count | an upper bound on simultaneous hardware breakpoints, independent of tooling |
| Intel PT capability leaf | `CPUID.14H.0` returns max sub-leaf 1 and capability `EBX = 0x0000005F` | the leaf answers; the bits are printed raw because their meanings belong to the processor specification for this family, not to this repository |
@@ -100,6 +101,7 @@ that has been exercised.
| Unpacking a packaged Python program | no packaged sample was analysed here; the container behaviour is documented by the packagers' own tooling |
| Recovering code objects from a versioned wrapper | no wrapped sample was obtained; the mechanism is quoted from one public analysis and is version-specific |
| Instruction-level reading of a listing | no target was disassembled here; the conventions are documented architecture, and a producer claim (compiled here, assembled there) is `inferred` until a build whose toolchain is known confirms it |
| CPU-vendor divergence | no AMD host was available; the divergences are stated from both vendors' specifications and one public CVE analysis, and nothing was measured across vendors |
## Where the corpus comes from
@@ -258,6 +258,9 @@ done here:
- **The deliverable for most requests that land in this file is a written conclusion**: which
layer can answer the question on this host, what it costs, and what is unavailable. That is
the honest output, and it is more useful than an instrument that was never going to load.
- **The mechanisms here are one vendor's.** The control structure, the entry instructions and the
second-level translation named above are Intel's; the other vendor's equivalents, and why a tool
built for one set does not drive the other, are in `references/cpu-vendor-divergence.md`.
## Verification for this shape
+16 -1
View File
@@ -12,7 +12,7 @@ That is the shape worth fearing. A crash announces itself; these do not.
Each entry states the failure, the mechanism that makes it invisible, and the check that catches it.
P1-P25 are format and mechanism failures of Windows binaries; P26-P37 are failures of the *analysis*
P1-P25 are format and mechanism failures of Windows binaries; P26-P38 are failures of the *analysis*
itself — sampling read as observation, a self-built parser trusted, a verification that could not have
failed. Those are written with Windows examples because that is where they were paid for, but the
mechanism does not depend on the platform: read them on a managed assembly and a driver the same way.
@@ -396,3 +396,18 @@ elevated, so the setting is not in the key the code names either.
`System32` and `SysWOW64` copies of the file. Record which view was searched next to the negative
result. "Not found" is a statement about one view; only both views together make it a statement
about the machine.
**P38. An undefined result recorded as a defined one, on the machine that happened to be under the desk.**
A value the instruction manual calls undefined — the destination of a zero-input `bsf`, the overflow
flag after a variable shift, the flags after a multiply — is whatever the part in front of you left
there. Measure it on one machine and it looks like a rule; write the rule into a specification or a
patch and it becomes a requirement the other vendor's part does not meet. The same shape covers a
vendor-string dispatch and a spin loop tuned to one part's `pause`: the code worked, on the machine
that produced the note, and nothing in the note says which machine that was.
*Check:* every timing, flag and fault result is recorded with the vendor string and the
family/model/stepping — `scripts/hwprobe.py` prints all three. A sentence that states a value where
the manual says undefined is rewritten as undefined before the artifact or the specification leaves
the analysis. When the same OS build behaves differently on two hosts, compare the two CPUs before
the two builds.
+3
View File
@@ -35,6 +35,7 @@ without a check.
| The file checksum no longer matches and something now refuses the binary | `references/patching.md` |
| Your signature-based patch worked on one version and misses on the next | `references/aob-and-signatures.md` |
| Two supposedly identical builds behave differently | `references/pe-diff-and-versions.md` |
| A check passes here and fails on the machine the deliverable is for, with the same OS build | `references/cpu-vendor-divergence.md` |
| The target is a .NET assembly and hex-patching it does nothing | `references/managed-dotnet.md` |
| A .NET assembly decompiles, but the names are gone or the C# does not add up | `references/dotnet-decompilation.md` |
| A Unity game's `global-metadata.dat` is missing, empty or unreadable beside its `GameAssembly.dll` | `references/unity-il2cpp.md` |
@@ -55,6 +56,7 @@ without a check.
| A detour or code cave crashes its callee, or the arguments arrive at wrong offsets | `references/assembly-idioms.md` |
| A 32-bit target's licence file or registry key is absent from the path the documentation gives | `references/assembly-idioms.md` |
| A prologue does not look like the compiler the headers named | `references/assembly-idioms.md` |
| A routine compares the CPUID vendor string, or branches on a flag the manual calls undefined | `references/cpu-vendor-divergence.md` |
| You are about to conclude "it must be server-side" | `references/network-activated-licensing.md` |
| You cannot tell whether a failure is your patch or the target's own behaviour | `references/verification.md` |
| You are about to write off a route as impossible | `references/protectors-and-packers.md` |
@@ -77,6 +79,7 @@ reference that is loaded at the wrong moment costs context and answers nothing.
| `references/static-analysis.md` | reading code: disassembly, decompilation, structure recovery, and where a listing lies |
| `references/compiler-fingerprints.md` | the toolchain is unidentified, or the call structure does not look like C or C++ |
| `references/assembly-idioms.md` | a listing must be read at the instruction level: the convention moves the arguments, the prologue says who produced the code, a string instruction stands in for a library routine, or a detour must preserve the frame |
| `references/cpu-vendor-divergence.md` | a result may belong to one processor's implementation rather than to x86-64: undefined flags, vendor-string dispatch, feature-gated decoding, system-instruction fault timing, or a specification about to be handed to another team |
| `references/pe-diff-and-versions.md` | two builds must be compared, or a finding has to survive a version change |
| `references/patching.md` | any byte edit: site choice, equal-length discipline, checksum, and proving the patch is reached |
| `references/aob-and-signatures.md` | a patch must be located across versions rather than at one address |