java-topology/defects/box2d/unit/Box2dTest.java
russell@unturf.com 0d33225dcc sdl/box2d: CWE-407 findings
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.
2026-03-30 09:25:48 -04:00

116 lines
4.5 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.");
}
}