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

@ -0,0 +1,105 @@
# UNDF: UNDF-2026-000001304
# CWE-407: Algorithmic Complexity — O(A^2*V + A*V) -> O(A + V) in
# RTTIGccClassRecoverer.getVttAddresses + isPossibleVttStart + addPointerToList
#
# Defect: Three coupled patterns in
# Ghidra/Features/Decompiler/ghidra_scripts/classrecovery/RTTIGccClassRecoverer.java
#
# 1. `isPossibleVttStart(address, vtables, knownVtts)` calls
# `getListOfVtableAndVftableTops(vtables)` on EVERY invocation, REBUILDING
# the full vtable+vftable address list from scratch. That's O(V) wasted work
# per call.
#
# 2. `getVttAddresses(vtables)` calls `isPossibleVttStart` once per address-to-check
# inside an outer `while (keepChecking)` retry loop, multiplying the rebuild cost.
#
# 3. Both `isPossibleVttStart` and `addPointerToList` use `List.contains` on
# `List<Address>` for membership: O(V) per check.
#
# Total per analysis: O(outer_iters * A * V) just for the rebuilds, plus
# O(A * V) for the linear-scan contains. For a gcc-compiled C++ binary
# with 1000 classes (2000 vtable+vftable addresses) and 500 candidate
# addresses, ~30M ops per RecoverClassesFromRTTIScript invocation.
#
# Fix: Hoist `vtableAndVftableAddrs` OUT of `isPossibleVttStart` into the
# caller `getVttAddresses` so it's built once. Convert both
# `vtableAndVftableAddrs` and `vttStarts` to `HashSet<Address>` for O(1)
# contains. Also pass the prebuilt set into `addPointerToList`.
#
# Complexity gate (defects/ghidra/bench/results.txt @ ghidra-0002):
# C=2000 A=1000: defective ~125ms, fixed <1ms (>=100x speedup)
# k-scaling 5x: time ratio must be <17.5x
--- a/Ghidra/Features/Decompiler/ghidra_scripts/classrecovery/RTTIGccClassRecoverer.java
+++ b/Ghidra/Features/Decompiler/ghidra_scripts/classrecovery/RTTIGccClassRecoverer.java
@@ -880,11 +880,15 @@ public class RTTIGccClassRecoverer extends RTTIClassRecoverer {
List<Address> vttStarts = new ArrayList<Address>();
+ // Build vtable+vftable address set ONCE, not on every isPossibleVttStart call.
+ // O(V) -> O(1) per membership check, eliminates O(A*V) rebuild waste.
+ Set<Address> vtableAndVftableSet =
+ new HashSet<>(getListOfVtableAndVftableTops(vtables));
+ Set<Address> vttStartSet = new HashSet<>();
boolean keepChecking = true;
int numToCheck = addressesToCheck.size();
while (keepChecking) {
for (Address possibleVttStart : addressesToCheck) {
monitor.checkCancelled();
- if (isPossibleVttStart(possibleVttStart, vtables, vttStarts)) {
+ if (isPossibleVttStart(possibleVttStart, vtableAndVftableSet, vttStartSet)) {
vttStarts.add(possibleVttStart);
+ vttStartSet.add(possibleVttStart);
}
}
@@ -940,18 +944,18 @@ public class RTTIGccClassRecoverer extends RTTIClassRecoverer {
private void addPointerToList(List<Vtt> vtts, List<Vtable> vtables)
throws CancelledException {
- List<Address> vtableAndVftableAddrs = getListOfVtableAndVftableTops(vtables);
- List<Address> vttStarts = getVttAddresses(vtts);
+ // Build sets once for the whole vtt-pointer walk: O(V+T) build, O(1) contains.
+ Set<Address> vtableAndVftableSet =
+ new HashSet<>(getListOfVtableAndVftableTops(vtables));
+ Set<Address> vttStartSet = new HashSet<>(getVttAddresses(vtts));
for (Vtt vtt : vtts) {
monitor.checkCancelled();
Address pointerAddress = vtt.getAddress();
Address referencedAddress = getReferencedAddress(pointerAddress);
while (referencedAddress != null &&
- (vtableAndVftableAddrs.contains(referencedAddress) ||
+ (vtableAndVftableSet.contains(referencedAddress) ||
referencedAddress.equals(vtt.getAddress()) ||
isSelfReferencing(pointerAddress) ||
- vttStarts.contains(referencedAddress))) {
+ vttStartSet.contains(referencedAddress))) {
vtt.addPointerToList(referencedAddress);
pointerAddress = pointerAddress.add(defaultPointerSize);
referencedAddress = getReferencedAddress(pointerAddress);
@@ -985,18 +989,15 @@ public class RTTIGccClassRecoverer extends RTTIClassRecoverer {
return vttStarts;
}
- private boolean isPossibleVttStart(Address address, List<Vtable> vtables,
- List<Address> knownVtts) throws CancelledException {
-
- // make list of all vtable tops and vftable tops
- List<Address> vtableAndVftableAddrs = getListOfVtableAndVftableTops(vtables);
-
+ private boolean isPossibleVttStart(Address address, Set<Address> vtableAndVftableSet,
+ Set<Address> knownVttSet) throws CancelledException {
+ // Caller (getVttAddresses) builds the sets once and passes them in.
+ // Eliminates O(V) rebuild on every call.
if (isSelfReferencing(address)) {
return true;
}
Address referencedAddress = getReferencedAddress(address);
- if (referencedAddress != null && (vtableAndVftableAddrs.contains(referencedAddress) ||
- knownVtts.contains(referencedAddress))) {
+ if (referencedAddress != null && (vtableAndVftableSet.contains(referencedAddress) ||
+ knownVttSet.contains(referencedAddress))) {
return true;
}
return false;
}