sdl-0001: SDL_gamepad.c SDL_PrivateAddMappingForGUID — O(M) tail walk of s_pSupportedGamepads linked list on every mapping insert → O(M²) bulk load. SDL_GameControllerDB ships >30 000 entries; fix: tail pointer s_pLastSupportedGamepad. box2d-0001: broad_phase.c b2UnBufferMove — linear scan through moveArray to find proxy key on destroy (acknowledged by code comment) → O(N²) on bulk destroy. Fix: index map (proxyKey → slot) for O(1) swap-remove.
116 lines
4.5 KiB
Java
116 lines
4.5 KiB
Java
package unit;
|
||
|
||
import java.util.*;
|
||
|
||
/**
|
||
* Box2dTest — box2d-0001
|
||
*
|
||
* Proves CWE-407 in Box2D v3 (erincatto/box2d) src/broad_phase.c:
|
||
* box2d-0001: b2UnBufferMove — linear scan through moveArray when removing a
|
||
* buffered proxy key → O(N²) when N proxies are destroyed.
|
||
*
|
||
* The code itself has a comment: "Purge from move buffer. Linear search."
|
||
* with a todo note acknowledging the redundancy with the hash-based moveSet.
|
||
*
|
||
* Run: javac -d . Box2dTest.java && java -ea unit.Box2dTest
|
||
*/
|
||
public class Box2dTest {
|
||
|
||
// ── box2d-0001: b2UnBufferMove linear scan ───────────────────────────────
|
||
|
||
/**
|
||
* SLOW: simulates b2UnBufferMove — linear scan through moveArray.
|
||
* For each destroy call: scan the array from index 0 to find the key,
|
||
* then swap-remove (O(1) remove once found, but O(N) to find).
|
||
* Destroys all N proxies → O(N²) total.
|
||
*/
|
||
static long bulkDestroySlow(int proxyCount) {
|
||
// moveArray: list of buffered proxy keys
|
||
List<Integer> moveArray = new ArrayList<>();
|
||
for (int i = 0; i < proxyCount; i++) {
|
||
moveArray.add(i); // b2BufferMove: add proxy to moveArray
|
||
}
|
||
|
||
long ops = 0;
|
||
// Destroy all proxies in reverse order (worst-case scan direction)
|
||
for (int proxyKey = proxyCount - 1; proxyKey >= 0; proxyKey--) {
|
||
// b2UnBufferMove: linear scan to find proxyKey in moveArray
|
||
int size = moveArray.size();
|
||
for (int i = 0; i < size; i++) {
|
||
ops++;
|
||
if (moveArray.get(i) == proxyKey) {
|
||
// swap-remove: O(1)
|
||
int last = moveArray.get(moveArray.size() - 1);
|
||
moveArray.set(i, last);
|
||
moveArray.remove(moveArray.size() - 1);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
/**
|
||
* FAST: index map (proxyKey → slot in moveArray) → O(1) lookup per remove.
|
||
* Simulates the fix: maintain a HashMap<proxyKey, slot>.
|
||
*/
|
||
static long bulkDestroyFast(int proxyCount) {
|
||
List<Integer> moveArray = new ArrayList<>();
|
||
Map<Integer, Integer> moveIndex = new HashMap<>(); // proxyKey → array slot
|
||
|
||
for (int i = 0; i < proxyCount; i++) {
|
||
moveIndex.put(i, moveArray.size());
|
||
moveArray.add(i);
|
||
}
|
||
|
||
long ops = 0;
|
||
for (int proxyKey = proxyCount - 1; proxyKey >= 0; proxyKey--) {
|
||
ops++; // O(1) hash lookup
|
||
Integer slot = moveIndex.remove(proxyKey);
|
||
if (slot != null && slot < moveArray.size()) {
|
||
// swap-remove: O(1)
|
||
int last = moveArray.get(moveArray.size() - 1);
|
||
moveArray.set(slot, last);
|
||
moveIndex.put(last, slot);
|
||
moveArray.remove(moveArray.size() - 1);
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
static void testBox2d0001() {
|
||
System.out.println("=== box2d-0001: b2UnBufferMove linear scan on bulk destroy ===");
|
||
|
||
int[] sizes = {100, 500, 1000, 5000, 10000};
|
||
for (int N : sizes) {
|
||
long slow = bulkDestroySlow(N);
|
||
long fast = bulkDestroyFast(N);
|
||
double ratio = (double) slow / fast;
|
||
System.out.printf(" N=%6d slow_ops=%12d fast_ops=%8d ratio=%.1fx%n",
|
||
N, slow, fast, ratio);
|
||
}
|
||
|
||
// Regression: at N=1000 slow >> fast
|
||
long slow1000 = bulkDestroySlow(1000);
|
||
long fast1000 = bulkDestroyFast(1000);
|
||
double ratio1000 = (double) slow1000 / fast1000;
|
||
assert ratio1000 > 50.0 :
|
||
"box2d-0001 FAIL: expected ratio > 50x at N=1000, got " + ratio1000;
|
||
System.out.println(" [PASS] box2d-0001: ratio=" + String.format("%.1f", ratio1000) + "x at N=1000");
|
||
|
||
// Verify O(N²) vs O(N): ratio at N=5000 should be >> ratio at N=1000
|
||
long slow5000 = bulkDestroySlow(5000);
|
||
long fast5000 = bulkDestroyFast(5000);
|
||
double ratio5000 = (double) slow5000 / fast5000;
|
||
assert ratio5000 > ratio1000 * 3 :
|
||
"box2d-0001 FAIL: O(N²) growth expected; ratio5000=" + ratio5000 + " ratio1000=" + ratio1000;
|
||
System.out.println(" [PASS] box2d-0001: O(N²) growth confirmed (ratio5000=" +
|
||
String.format("%.1f", ratio5000) + "x > 3×ratio1000=" +
|
||
String.format("%.1f", ratio1000) + "x)");
|
||
}
|
||
|
||
public static void main(String[] args) {
|
||
testBox2d0001();
|
||
System.out.println("\nAll Box2D tests PASS.");
|
||
}
|
||
}
|