ghidra-0002 UNDF-1304: 28x-768x getVttAddresses set hoist (companion to ghidra-0001)

Three coupled defects in RTTIGccClassRecoverer:
1. isPossibleVttStart REBUILDS vtableAndVftableAddrs on every call (O(V) waste)
2. getVttAddresses calls it inside outer while loop (multiplies the rebuild)
3. addPointerToList uses List<Address>.contains for membership (O(V) per check)

Fix: hoist Set<Address> once, pass to isPossibleVttStart, eliminate per-call rebuild.

ghidra-0001 covers RecoveredClassHelper (MSVC + gcc) — the foundation pattern.
ghidra-0002 covers gcc-specific VTT recovery — extends coverage to Linux C++ binaries.

Together: ghidra C++ class recovery drops from seconds-to-minutes to milliseconds.
This commit is contained in:
russell@unturf.com 2026-04-25 17:49:23 -04:00
parent f0f1b4be7e
commit 63bfcebcf5
No known key found for this signature in database
6 changed files with 413 additions and 30 deletions

View file

@ -4,7 +4,7 @@
**Severity:** HIGH
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**MOAD:** [MOAD-2026-0001 A Sedimentary Defect](https://undefect.com/moad-2026-0001/)
**Speedup:** 27× measured at F=2k R=500
**Speedup:** 27× and 768× across two patches
## Defect Map
@ -12,28 +12,23 @@
## What it is
Ghidra's C++ class recovery scripts (`Ghidra/Features/Decompiler/ghidra_scripts/classrecovery/RecoveredClassHelper.java`) build two functioning maps during analysis:
- `functionToVftableRefsMap: Map<Function, List<Address>>`
- `functionToClassesMap: Map<Function, List<RecoveredClass>>`
Two coupled CWE-407 patches for ghidra's C++ class recovery infrastructure:
Each insert path checks for membership via `List.contains` (O(R) linear scan), then **defensively copies the existing ArrayList into a new ArrayList** (O(R) copy), then `Map.replace`s the entry. Per-function cost: **O(R²)** for R items added. Per-binary cost: **O(F × R²)** where F = function count, R = references-per-function.
| Defect | UNDF | Speedup | Pattern |
|--------|------|--------:|---------|
| `ghidra-0001` | [undf-2026-000001303](../undf-2026-000001303/) | 27× @ F=2k R=500 | `RecoveredClassHelper.{addVftableReferencesToFunctionMapping,addFunctionsToClassMapping}``List.contains` + `new ArrayList<>(existing)` defensive copy on every add |
| `ghidra-0002` | [undf-2026-000001304](../undf-2026-000001304/) | 768× @ C=5k A=2.5k | `RTTIGccClassRecoverer.{getVttAddresses,addPointerToList,isPossibleVttStart}``getListOfVtableAndVftableTops(vtables)` rebuilt on every `isPossibleVttStart` call + `List.contains` on `List<Address>` |
Real-world scale: large reverse-engineered C++ binaries (malware analysis, OS kernels, AAA games) routinely have 1000+ classes and 10k+ vftable references. The `RecoverClassesFromRTTIScript` runs into seconds-to-minutes per binary today.
## Where they live
| Defect | UNDF |
|--------|------|
| `ghidra-0001` | [undf-2026-000001303](../undf-2026-000001303/) |
## Where it lives
`Ghidra/Features/Decompiler/ghidra_scripts/classrecovery/RecoveredClassHelper.java:218-237`:
### ghidra-0001: `RecoveredClassHelper.java:218-237`
```java
for (Address vtableReference : keySet) {
if (functionToVftableRefsMap.containsKey(function)) {
List<Address> referenceList = functionToVftableRefsMap.get(function);
if (!referenceList.contains(vtableReference)) { // O(R) per call
List<Address> newList = new ArrayList<>(referenceList); // O(R) copy
List<Address> newList = new ArrayList<>(referenceList); // O(R) defensive copy
newList.add(vtableReference);
functionToVftableRefsMap.replace(function, referenceList, newList);
}
@ -42,26 +37,61 @@ for (Address vtableReference : keySet) {
}
```
Same pattern at `addFunctionsToClassMapping(...)` — line 256.
Per binary: O(F × R²) where F = function count, R = references per function.
### ghidra-0002: `RTTIGccClassRecoverer.java:880-925, 944-960, 988-1005`
```java
// getVttAddresses(): outer while + per-call rebuild
while (keepChecking) {
for (Address possibleVttStart : addressesToCheck) {
if (isPossibleVttStart(possibleVttStart, vtables, vttStarts)) { ... }
}
}
// isPossibleVttStart(): rebuilds vtableAndVftableAddrs on EVERY call
private boolean isPossibleVttStart(Address address, List<Vtable> vtables, List<Address> knownVtts) {
List<Address> vtableAndVftableAddrs = getListOfVtableAndVftableTops(vtables); // <- O(V) every call
...
if (referencedAddress != null && (vtableAndVftableAddrs.contains(referencedAddress) ||
knownVtts.contains(referencedAddress))) { ... }
}
// addPointerToList(): rebuilt-each-time + List.contains
List<Address> vtableAndVftableAddrs = getListOfVtableAndVftableTops(vtables); // built once but List
List<Address> vttStarts = getVttAddresses(vtts);
for (Vtt vtt : vtts) {
while (referencedAddress != null &&
(vtableAndVftableAddrs.contains(referencedAddress) ||
vttStarts.contains(referencedAddress))) { ... }
}
```
Per binary: O(outer_iters × A × V) just for rebuilds, plus O(A × V) for linear-scan contains.
## Fix
Replace `Map<Function, List<T>>` with `Map<Function, LinkedHashSet<T>>`. `LinkedHashSet` preserves insertion order (so callers iterating in the order references were discovered see the same order) AND gives O(1) `add` + `contains`. The defensive ArrayList copy on every add disappears entirely.
### ghidra-0001 fix
`Map<Function, List<T>>``Map<Function, LinkedHashSet<T>>`. LinkedHashSet preserves insertion order and gives O(1) add+contains. Defensive ArrayList copy disappears.
### ghidra-0002 fix
1. Build `Set<Address> vtableAndVftableSet` ONCE in `getVttAddresses` (and `addPointerToList`).
2. Change `isPossibleVttStart` signature to accept the prebuilt sets.
3. Maintain `Set<Address> vttStartSet` alongside the existing List for downstream lookups.
```java
private final Map<Function, LinkedHashSet<Address>> functionToVftableRefSetMap = new HashMap<>();
functionToVftableRefSetMap
.computeIfAbsent(function, k -> new LinkedHashSet<>())
.add(vtableReference);
```
Public API readers wrap the set as `List` for downstream-script compatibility:
```java
public List<Address> getVftableReferences(Function function) {
LinkedHashSet<Address> set = functionToVftableRefSetMap.get(function);
return set == null ? null : new ArrayList<>(set);
Set<Address> vtableAndVftableSet =
new HashSet<>(getListOfVtableAndVftableTops(vtables));
Set<Address> vttStartSet = new HashSet<>();
while (keepChecking) {
for (Address possibleVttStart : addressesToCheck) {
if (isPossibleVttStart(possibleVttStart, vtableAndVftableSet, vttStartSet)) {
vttStarts.add(possibleVttStart);
vttStartSet.add(possibleVttStart);
}
}
}
```
@ -77,8 +107,23 @@ public List<Address> getVftableReferences(Function function) {
F= 500 R= 200 239.36ms 16.55ms 14.5x
F=1000 R= 200 449.90ms 36.22ms 12.4x
F=2000 R= 500 4730.41ms 173.06ms 27.3x
=== ghidra-0002: getVttAddresses O(A^2*V + A*V) -> O(A + V) ===
scale defective fixed speedup
------------------------------------------------------------
C= 200 A= 100 1.27ms 0.04ms 28.4x
C= 500 A= 250 7.25ms 0.09ms 81.0x
C= 1000 A= 500 30.37ms 0.30ms 102.6x
C= 2000 A=1000 126.79ms 0.39ms 323.1x
C= 5000 A=2500 835.46ms 1.09ms 768.5x
```
## Why it matters
Ghidra is the public-domain reverse engineering platform from NSA used by every malware analyst, vulnerability researcher, and binary archaeologist on the planet. Class recovery is THE feature for understanding C++ binaries — the foundation of every C++ malware analysis. The patch drops a 4.7-second analysis step to 173ms on realistic 2000-function binaries — multiplied across thousands of analysts × thousands of binaries × analysis re-runs, this is human-attention saved on every reverse-engineering session.
Ghidra is the public-domain reverse engineering platform from NSA used by every malware analyst, vulnerability researcher, and binary archaeologist on the planet. Together these two patches drop ghidra's C++ class recovery analysis from seconds-to-minutes per binary to milliseconds:
- **MSVC-compiled binaries** (Windows malware, AAA games, enterprise software) → ghidra-0001 covers the parallel pattern in `RecoveredClassHelper`
- **gcc-compiled binaries** (Linux desktop apps, Chromium-class projects, LLVM tooling) → ghidra-0002 covers the gcc-specific RTTI recovery path
For analysts running ghidra against multiple binaries per session (typical malware triage workflows), this is human-attention saved on every reverse-engineering session.