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.
This commit is contained in:
russell@unturf.com 2026-03-30 09:25:48 -04:00
parent db92c9428a
commit 0d33225dcc
4 changed files with 337 additions and 0 deletions

View file

@ -0,0 +1,61 @@
# UNDF: (leave blank)
Box2D v3 CWE-407: b2UnBufferMove — linear scan through moveArray to find proxy → O(N²) on bulk destroy
b2UnBufferMove (broad_phase.c) is called from b2BroadPhase_DestroyProxy.
It uses b2RemoveKey() on the hash-based moveSet (O(1)) but then performs a
separate linear scan through bp->moveArray to find and remove the same key:
// Purge from move buffer. Linear search.
// todo if I can iterate the move set then I don't need the moveArray
for (int i = 0; i < count; ++i) {
if (bp->moveArray.data[i] == proxyKey) {
b2IntArray_RemoveSwap(&bp->moveArray, i);
break;
}
}
The code comment acknowledges the linear scan. When N proxies are buffered in
moveArray and a game destroys all of them (e.g., scene teardown, level reload),
b2BroadPhase_DestroyProxy is called N times, each scanning up to N entries →
O(N²) total. At N=10 000 (large physics scene), this is 100 000 000 iterations.
The moveSet already provides O(1) membership; a parallel index map
(proxyKey → moveArray position) would reduce the scan to O(1) with swap-remove.
Severity: HIGH — hits on every scene reload / bulk body destruction.
Complexity: O(N²) → O(1) per remove with an index map.
--- a/src/broad_phase.c
+++ b/src/broad_phase.c
@@ DEFECT box2d-0001: b2UnBufferMove linear scan @@
static inline void b2UnBufferMove( b2BroadPhase* bp, int proxyKey )
{
bool found = b2RemoveKey( &bp->moveSet, proxyKey + 1 );
if ( found )
{
- // Purge from move buffer. Linear search.
- // todo if I can iterate the move set then I don't need the moveArray
- int count = bp->moveArray.count;
- for ( int i = 0; i < count; ++i )
- {
- if ( bp->moveArray.data[i] == proxyKey )
- {
- b2IntArray_RemoveSwap( &bp->moveArray, i );
- break;
- }
- }
+ // FIX box2d-0001: O(1) lookup via index map (proxyKey → slot in moveArray)
+ // Requires a parallel b2HashTable<int,int> moveArrayIndex field in b2BroadPhase.
+ //
+ // Pattern:
+ // int slot = b2MoveIndexTable_Get(&bp->moveIndexTable, proxyKey);
+ // int last = bp->moveArray.data[bp->moveArray.count - 1];
+ // bp->moveArray.data[slot] = last;
+ // b2MoveIndexTable_Set(&bp->moveIndexTable, last, slot);
+ // b2MoveIndexTable_Remove(&bp->moveIndexTable, proxyKey);
+ // --bp->moveArray.count;
}
}

View file

@ -0,0 +1,116 @@
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.");
}
}